Skip to content

feat: auto-close dispute UI when order reaches terminal state #503

Merged
grunch merged 6 commits into
mainfrom
auto-close-dispute
Mar 6, 2026
Merged

feat: auto-close dispute UI when order reaches terminal state #503
grunch merged 6 commits into
mainfrom
auto-close-dispute

Conversation

@Catrya

@Catrya Catrya commented Feb 27, 2026

Copy link
Copy Markdown
Member

fix #494

  • Auto-close dispute when seller releases sats (settledHoldInvoice/success)
  • Auto-close dispute on cooperative cancellation (canceled)
  • Differentiate closure reason via dispute.action: 'user-completed' vs 'cooperative-cancel'
  • Show distinct messages in dispute list and detail view per closure type
  • Add localization keys in en/es/it/fr for both closure messages

Summary by CodeRabbit

  • New Features

    • Disputes auto-close when orders reach terminal completion or cooperative-cancellation states.
    • UI shows distinct messages for "user-completed" and "cooperative-cancel" closures.
    • Trade/status UI treats cooperative-cancel as "Canceling" (waiting) instead of inactive/canceled.
    • Added localized strings for the new status and closure messages (EN/ES/FR/IT).
  • Documentation

    • Added guidance explaining dispute auto-close behavior and rationale.
  • Tests

    • Added tests covering dispute auto-close scenarios and localized description mapping.

  - Auto-close dispute when seller releases sats (settledHoldInvoice/success)
  - Auto-close dispute on cooperative cancellation (canceled)
  - Differentiate closure reason via dispute.action: 'user-completed' vs 'cooperative-cancel'
  - Show distinct messages in dispute list and detail view per closure type
  - Add localization keys in en/es/it/fr for both closure messages
@coderabbitai

coderabbitai Bot commented Feb 27, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7e08f75 and 8950a99.

📒 Files selected for processing (3)
  • docs/architecture/ORDER_STATUS_HANDLING.md
  • lib/features/order/models/order_state.dart
  • test/features/order/models/order_state_dispute_auto_close_test.dart
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/features/order/models/order_state.dart
  • docs/architecture/ORDER_STATUS_HANDLING.md

Walkthrough

When an order reaches a terminal state (success / settledHoldInvoice) or is cooperatively canceled, the app auto-marks any associated in-progress dispute as closed with action user-completed or cooperative-cancel; the dispute model and UI select new action-specific localized closed messages and trades/list chips reflect a "canceling" pending state.

Changes

Cohort / File(s) Summary
Dispute Model
lib/data/models/dispute.dart
Treat closed as a resolved state and map action values (user-completed, cooperative-cancel) to distinct description keys.
Dispute UI
lib/features/disputes/widgets/dispute_status_content.dart
Render disputeClosedUserCompleted or disputeClosedCooperativeCancel when resolved/closed dispute action matches those values; keep existing fallbacks.
Order State Auto-Close
lib/features/order/models/order_state.dart
Add auto-close logic in OrderState.updateWith: when order reaches terminal statuses (success / settledHoldInvoice) or canceled with cooperative-cancel action, set associated dispute to closed with appropriate action and log the auto-close.
Trades UI
lib/features/trades/widgets/trades_list_item.dart
Handle Status.cooperativelyCanceled as a separate pending case showing a waiting background and canceling label instead of the previous canceled styling.
Localization
lib/l10n/intl_en.arb, lib/l10n/intl_es.arb, lib/l10n/intl_fr.arb, lib/l10n/intl_it.arb
Add cancelingStatus, disputeClosedUserCompleted, and disputeClosedCooperativeCancel localization keys across languages.
Docs
docs/architecture/ORDER_STATUS_HANDLING.md
Document "Dispute Auto-Close on Terminal State" behavior, triggers, UI impacts, rationale, and chip label rationale.
Tests
test/features/order/models/order_state_dispute_auto_close_test.dart
Add tests covering auto-close transitions, idempotency, preservation of dispute fields, and localized description mapping.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant App as Mobile App
    participant OrderState as OrderState
    participant DisputeState as DisputeState
    participant UI as Dispute UI

    User->>App: Completes or cooperatively cancels order
    App->>OrderState: Order transitions to terminal state (success / settledHoldInvoice / canceled)
    OrderState->>OrderState: Find dispute by orderId
    alt dispute exists and not already terminal
        OrderState->>DisputeState: Set dispute.status = "closed", action = "user-completed" / "cooperative-cancel"
    end
    DisputeState->>UI: Emit updated dispute state
    UI->>UI: Select localized message based on dispute.action
    UI->>App: Render closed dispute message / update chips
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • AndreaDiazCorreia
  • grunch

Poem

🐇 I hopped through orders, sniffed the end,
Parties finished—no more to mend.
I closed the thread with a joyful twitch,
Carrots and peace—no more dispute glitch. 🥕

🚥 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 PR title accurately summarizes the main objective: auto-closing disputes when orders reach terminal states, which is the core feature implemented across multiple files.
Linked Issues check ✅ Passed The PR fully addresses issue #494 by implementing auto-close logic in OrderState.updateWith() for terminal states (success, settledHoldInvoice, canceled) with appropriate dispute actions (user-completed, cooperative-cancel) and updating UI components with localized messages.
Out of Scope Changes check ✅ Passed All changes are in scope: dispute auto-close logic, UI updates for dispute status display, localization strings, and status handling for cooperativelyCanceled state. One minor addition is the 'cancelingStatus' localization key for the 'Canceling' chip label, which relates to the pending cooperative cancellation UI indicator mentioned in the commit messages.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch auto-close-dispute

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 `@lib/features/order/models/order_state.dart`:
- Around line 194-213: The auto-close branch that sets updatedDispute to action
'cooperative-cancel' currently fires whenever newStatus == Status.canceled;
change it to only run for cooperative cancellation flows by adding a check for
the triggering cancellation action (e.g., require the cancellation Action to be
the cooperative cancel action rather than Action.cancel /
Action.holdInvoicePaymentCanceled / Action.adminCancel). Update the if condition
around updatedDispute.copyWith(... action: 'cooperative-cancel' ...) to verify
the specific cooperative cancellation action (use the existing Action
enum/variable used when status transitions occur) before assigning
'cooperative-cancel' and logging the cooperative message.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4f1bde5 and f98f06f.

📒 Files selected for processing (7)
  • lib/data/models/dispute.dart
  • lib/features/disputes/widgets/dispute_status_content.dart
  • lib/features/order/models/order_state.dart
  • lib/l10n/intl_en.arb
  • lib/l10n/intl_es.arb
  • lib/l10n/intl_fr.arb
  • lib/l10n/intl_it.arb

Comment thread lib/features/order/models/order_state.dart

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
lib/features/order/models/order_state.dart (1)

194-214: LGTM! Past review concern properly addressed.

The cooperative-cancel branch now correctly guards against non-cooperative cancellation actions by checking message.action == Action.cooperativeCancelAccepted (line 208). This ensures 'cooperative-cancel' is only assigned for actual cooperative cancellations, not for Action.cancel, Action.holdInvoicePaymentCanceled, or Action.adminCancel.

Optional refactor: The terminal dispute status list is duplicated. Consider extracting it:

♻️ Optional: Extract constant for terminal dispute statuses
+  static const _terminalDisputeStatuses = {'resolved', 'seller-refunded', 'closed'};
+
   // Auto-close dispute when order reaches terminal state by user action
   if (updatedDispute != null &&
-      !const ['resolved', 'seller-refunded', 'closed']
-          .contains(updatedDispute.status?.toLowerCase()) &&
+      !_terminalDisputeStatuses.contains(updatedDispute.status?.toLowerCase()) &&
       const [Status.success, Status.settledHoldInvoice].contains(newStatus)) {
     // ...
   } else if (updatedDispute != null &&
-      !const ['resolved', 'seller-refunded', 'closed']
-          .contains(updatedDispute.status?.toLowerCase()) &&
+      !_terminalDisputeStatuses.contains(updatedDispute.status?.toLowerCase()) &&
       newStatus == Status.canceled &&
       message.action == Action.cooperativeCancelAccepted) {

,

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

In `@lib/features/order/models/order_state.dart` around lines 194 - 214, Duplicate
literal list of terminal dispute statuses should be extracted to a single
constant; create a top-level const like terminalDisputeStatuses = ['resolved',
'seller-refunded', 'closed'] and replace both occurrences of the inline const
list in the updatedDispute guards (the first if and the else-if that check
updatedDispute.status?.toLowerCase()) with terminalDisputeStatuses to remove
duplication and improve readability while keeping the existing logic in the
order state update flow (refer to the updatedDispute null checks and the
newStatus/ message.action conditions).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@lib/features/order/models/order_state.dart`:
- Around line 194-214: Duplicate literal list of terminal dispute statuses
should be extracted to a single constant; create a top-level const like
terminalDisputeStatuses = ['resolved', 'seller-refunded', 'closed'] and replace
both occurrences of the inline const list in the updatedDispute guards (the
first if and the else-if that check updatedDispute.status?.toLowerCase()) with
terminalDisputeStatuses to remove duplication and improve readability while
keeping the existing logic in the order state update flow (refer to the
updatedDispute null checks and the newStatus/ message.action conditions).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f98f06f and 9eefcd1.

📒 Files selected for processing (2)
  • docs/architecture/ORDER_STATUS_HANDLING.md
  • lib/features/order/models/order_state.dart
✅ Files skipped from review due to trivial changes (1)
  • docs/architecture/ORDER_STATUS_HANDLING.md

@mostronatorcoder mostronatorcoder Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review: Auto-close dispute UI on terminal state

Overall assessment

The approach is sound — inferring dispute closure from order terminal state instead of subscribing to kind 38386 is pragmatic and avoids protocol expansion. The documentation is excellent and the rationale is well-articulated. However, there are issues that need addressing.


🔴 Critical: No tests

Zero test files were added or modified. This PR introduces branching logic in OrderState.updateWith() that determines dispute closure behavior based on status + action combinations. This is exactly the kind of logic that needs unit tests.

The repo's AGENTS.md states: "Add targeted scenarios when expanding complex logic or async workflows."

At minimum, tests should cover:

  1. Dispute auto-closes to user-completed when status reaches success
  2. Dispute auto-closes to user-completed when status reaches settledHoldInvoice
  3. Dispute auto-closes to cooperative-cancel when cooperativeCancelAccepted
  4. Already-resolved dispute is NOT overwritten (status resolved stays resolved)
  5. Already-closed dispute is NOT overwritten (idempotency)
  6. Dispute with status seller-refunded is NOT overwritten
  7. getLocalizedDescription returns correct message for user-completed action
  8. getLocalizedDescription returns correct message for cooperative-cancel action

🟡 DRY violation: duplicated guard condition

In order_state.dart lines 195-213, the guard condition is duplicated:

!const ['resolved', 'seller-refunded', 'closed']
    .contains(updatedDispute.status?.toLowerCase())

This appears twice in the if/else-if chain. Extract to a local variable:

final disputeAlreadyTerminal = const ['resolved', 'seller-refunded', 'closed']
    .contains(updatedDispute?.status?.toLowerCase());

if (updatedDispute != null && !disputeAlreadyTerminal &&
    const [Status.success, Status.settledHoldInvoice].contains(newStatus)) {
  // ...
} else if (updatedDispute != null && !disputeAlreadyTerminal &&
    newStatus == Status.canceled &&
    message.action == Action.cooperativeCancelAccepted) {
  // ...
}

🟡 Magic strings for dispute status

The code uses raw strings 'closed', 'resolved', 'seller-refunded', 'user-completed', 'cooperative-cancel', 'admin-settled', 'admin-canceled' scattered across multiple files. These should be constants (e.g., on Dispute or a DisputeStatus class) to prevent typos and enable refactoring. This is a pre-existing problem that this PR extends — not a blocker for this PR specifically, but worth noting as tech debt.

🟡 closed maps to DisputeDescriptionKey.resolved — misleading?

In dispute.dart line 500, 'closed' falls into the same case as 'resolved':

case 'resolved':
case 'solved':
case 'closed':
  return DisputeDescriptionKey.resolved;

Then in getLocalizedDescription, the action field disambiguates. This works but is fragile — the description key says "resolved" when the dispute is actually "closed". If anyone adds logic that checks descriptionKey == DisputeDescriptionKey.resolved expecting admin resolution, it will incorrectly match user-closed disputes too. Consider adding a DisputeDescriptionKey.closed value to make the distinction explicit.

✅ What looks good

  • CI passes, no conflicts
  • Documentation is thorough — the ORDER_STATUS_HANDLING.md additions explain the "why" clearly
  • Localization covers all 4 languages (EN/ES/FR/IT)
  • cooperativelyCanceled → "Canceling" (orange) is correct — it IS a pending state, not a final one
  • The guard against overwriting already-terminal disputes is correct
  • Using message.action == Action.cooperativeCancelAccepted to distinguish cooperative cancel from admin cancel is precise

Verdict

Request Changes — primarily for the missing tests. The auto-close logic in OrderState.updateWith() is critical path code for dispute handling. It must have test coverage before merging.

@mostronatorcoder mostronatorcoder Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All requested changes addressed in 8950a99:

  • Tests added: 12 tests covering all 8 requested scenarios + 4 extra edge cases (admin-canceled, field preservation, no-dispute-present, description key mapping)
  • DRY guard extracted: disputeAlreadyTerminal local variable replaces duplicated condition
  • ✅ CI passes

The magic strings note (closed, resolved, seller-refunded etc.) remains as acknowledged tech debt — not a blocker for this PR.

LGTM 👍

@grunch grunch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@grunch grunch merged commit e4ebe4a into main Mar 6, 2026
2 of 3 checks passed
@grunch grunch deleted the auto-close-dispute branch March 6, 2026 01:07
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.

feat: auto-close dispute UI when order reaches terminal state

2 participants