Skip to content

feat: implement raise dispute flow with redirect to dispute review page#256

Open
od-hunter wants to merge 1 commit into
boundlessfi:mainfrom
od-hunter:feat/raise-dispute
Open

feat: implement raise dispute flow with redirect to dispute review page#256
od-hunter wants to merge 1 commit into
boundlessfi:mainfrom
od-hunter:feat/raise-dispute

Conversation

@od-hunter
Copy link
Copy Markdown
Contributor

@od-hunter od-hunter commented May 29, 2026

Closes #203


#203

Summary by CodeRabbit

  • New Features
    • Added dispute functionality allowing users to raise disputes on bounties with customizable reason and description
    • Integrated dispute submission with real-time validation, loading states, and success/error notifications
    • Secure authenticated endpoint that validates input and communicates with backend services
    • Upon successful dispute creation, users are automatically redirected to the dispute detail page

Review Change Stack

- Add useRaiseDispute mutation hook in hooks/use-bounty-application.ts
  that POSTs to /api/disputes with bountyId, reason, and description,
  then invalidates the bounty detail and list queries on success

- Create app/api/disputes/route.ts Next.js API route handler that
  authenticates the user, validates input, and proxies the request
  to the backend REST endpoint with the auth token

- Enable the Raise Dispute button in bounty-detail-sidebar-cta.tsx
  (removes disabled + Coming Soon label), wires up an AlertDialog
  with a reason Select (DisputeReasonEnum) and description Textarea,
  inline validation that keeps the dialog open on empty fields, a
  loading spinner during submission, and on success closes the dialog,
  shows a toast, and redirects to /dispute/{newDisputeId}

- Document the RaiseDispute operation in admin-dispute.graphql
@vercel
Copy link
Copy Markdown

vercel Bot commented May 29, 2026

@od-hunter is attempting to deploy a commit to the Threadflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

📝 Walkthrough

Walkthrough

This PR implements a complete "Raise a Dispute" feature. A new POST /api/disputes endpoint validates and forwards dispute requests to the backend. A client-side useRaiseDispute React Query hook calls this endpoint and refreshes bounty queries on success. The bounty detail sidebar now renders an interactive form dialog that collects dispute reason and description, submits via the hook, and navigates to the created dispute detail page.

Changes

Raise Dispute Flow

Layer / File(s) Summary
Backend API Endpoint
app/api/disputes/route.ts
POST /api/disputes handler authenticates the user, validates campaignId, reason, and description fields, forwards the request to the backend REST endpoint with an optional Bearer token, and returns the created AdminDisputeDto with HTTP 201 on success or error codes on failure.
Client Mutation Hook and Types
hooks/use-bounty-application.ts
useRaiseDispute hook submits dispute data to /api/disputes and invalidates bounty detail and list queries on success; exports RaiseDisputeInput and RaiseDisputeResult types for the mutation payload and response shape; imports DisputeReasonEnum for type support.
Sidebar Form Dialog and Submit Logic
components/bounty-detail/bounty-detail-sidebar-cta.tsx
Adds local state for dispute dialog, selected reason, and description; defines DISPUTE_REASON_LABELS mapping for human-readable enum display; implements handleRaiseDispute with inline field validation, mutation call, form/dialog state reset, toast feedback, and navigation to the dispute page; adds "Raise a Dispute" button that opens an AlertDialog with reason <Select>, description <Textarea>, and loading submit button.
Integration Documentation
lib/graphql/operations/admin-dispute.graphql
Documents that the dispute creation flow is triggered from hooks/use-bounty-application.ts via REST POST /disputes endpoint, not exposed via GraphQL schema, and the server returns AdminDisputeDto with id for client-side redirect.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • boundlessfi/bounties#203: This PR directly implements the requested "Raise a Dispute" feature by adding the backend endpoint, client hook, UI form dialog, and wiring them together.

Possibly related PRs

  • boundlessfi/bounties#201: Both PRs modify the bounty sidebar "Raise a Dispute" flow in components/bounty-detail/bounty-detail-sidebar-cta.tsx, with this PR implementing the full feature and PR #201 handling related dispute workflow eligibility.

Suggested reviewers

  • Benjtalkshow
  • 0xdevcollins

Poem

🐰 A dispute flows forth from form to heart,
With reasons typed and descriptions smart,
From sidebar's call to detail's gleaming page,
The bounty's plea finds voice and stage.
Invalidate the cache, let fresh data flow—
The dispute is raised, and now the admins know! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: implementing a raise dispute flow with redirect functionality, which aligns with all four modified files and the PR objectives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@drips-wave
Copy link
Copy Markdown

drips-wave Bot commented May 29, 2026

@od-hunter Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/api/disputes/route.ts`:
- Around line 66-77: The fetch call that posts to `${backendUrl}/disputes` in
route.ts needs an AbortController-based timeout to avoid hanging; create an
AbortController, pass controller.signal into the fetch options for the
backendResponse call, start a setTimeout (e.g. 5–10s) that calls
controller.abort(), and clear the timeout after fetch completes (use
try/finally). Ensure you handle the abort/AbortError (from fetch of
backendResponse) and return an appropriate 504/timeout response instead of
leaving the request open.
- Around line 41-46: Validate the incoming reason against the DisputeReasonEnum
instead of accepting any string: in the POST handler in route.ts (where reason
is checked) replace the loose typeof check with a membership check against
DisputeReasonEnum (or a helper like isValidDisputeReason) and return
NextResponse.json({ error: "invalid reason", valid:
Object.values(DisputeReasonEnum) }, { status: 400 }) when it does not match so
invalid dispute reasons are rejected early before forwarding downstream.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ee8e4a10-c726-408b-a207-2423c2ca8a8e

📥 Commits

Reviewing files that changed from the base of the PR and between c56bd4b and 9ba0b9f.

📒 Files selected for processing (4)
  • app/api/disputes/route.ts
  • components/bounty-detail/bounty-detail-sidebar-cta.tsx
  • hooks/use-bounty-application.ts
  • lib/graphql/operations/admin-dispute.graphql

Comment thread app/api/disputes/route.ts
Comment on lines +41 to +46
if (!reason || typeof reason !== "string") {
return NextResponse.json(
{ error: "reason is required" },
{ status: 400 },
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject values outside DisputeReasonEnum.

This currently accepts any string and forwards it downstream, so invalid dispute reasons bypass server-side validation and fail later with a generic backend error.

Suggested fix
-import type { AdminDisputeDto, DisputeReasonEnum } from "`@/lib/graphql/generated`";
+import { DisputeReasonEnum } from "`@/lib/graphql/generated`";
+import type { AdminDisputeDto } from "`@/lib/graphql/generated`";
...
-    if (!reason || typeof reason !== "string") {
+    if (
+      !reason ||
+      typeof reason !== "string" ||
+      !Object.values(DisputeReasonEnum).includes(reason as DisputeReasonEnum)
+    ) {
       return NextResponse.json(
-        { error: "reason is required" },
+        { error: "reason must be a valid dispute reason" },
         { status: 400 },
       );
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/disputes/route.ts` around lines 41 - 46, Validate the incoming reason
against the DisputeReasonEnum instead of accepting any string: in the POST
handler in route.ts (where reason is checked) replace the loose typeof check
with a membership check against DisputeReasonEnum (or a helper like
isValidDisputeReason) and return NextResponse.json({ error: "invalid reason",
valid: Object.values(DisputeReasonEnum) }, { status: 400 }) when it does not
match so invalid dispute reasons are rejected early before forwarding
downstream.

Comment thread app/api/disputes/route.ts
Comment on lines +66 to +77
const backendResponse = await fetch(`${backendUrl}/disputes`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({
campaignId,
reason,
description,
}),
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a timeout to the backend proxy call.

The route waits on fetch() with no abort/timeout, so a slow or wedged backend can pin this request until the platform times it out.

Suggested fix
+    const controller = new AbortController();
+    const timeout = setTimeout(() => controller.abort(), 10_000);
+
-    const backendResponse = await fetch(`${backendUrl}/disputes`, {
-      method: "POST",
-      headers: {
-        "Content-Type": "application/json",
-        ...(token ? { Authorization: `Bearer ${token}` } : {}),
-      },
-      body: JSON.stringify({
-        campaignId,
-        reason,
-        description,
-      }),
-    });
+    let backendResponse: Response;
+    try {
+      backendResponse = await fetch(`${backendUrl}/disputes`, {
+        method: "POST",
+        signal: controller.signal,
+        headers: {
+          "Content-Type": "application/json",
+          ...(token ? { Authorization: `Bearer ${token}` } : {}),
+        },
+        body: JSON.stringify({
+          campaignId,
+          reason,
+          description,
+        }),
+      });
+    } finally {
+      clearTimeout(timeout);
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/disputes/route.ts` around lines 66 - 77, The fetch call that posts to
`${backendUrl}/disputes` in route.ts needs an AbortController-based timeout to
avoid hanging; create an AbortController, pass controller.signal into the fetch
options for the backendResponse call, start a setTimeout (e.g. 5–10s) that calls
controller.abort(), and clear the timeout after fetch completes (use
try/finally). Ensure you handle the abort/AbortError (from fetch of
backendResponse) and return an appropriate 504/timeout response instead of
leaving the request open.

Copy link
Copy Markdown
Contributor

@Benjtalkshow Benjtalkshow left a comment

Choose a reason for hiding this comment

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

The dispute flow itself is wired correctly: useRaiseDispute hook, REST proxy with auth check and validation, dialog with reason select and description textarea, inline validation, success path that closes the dialog and redirects to /dispute/{id}. All four acceptance criteria pass functionally.

Three things to fix before merge.

app/api/disputes/route.ts:3 imports graphqlRequest but never uses it. The PR uses REST since the GraphQL mutation isn't available yet. Remove the dead import.

Same file, line 63: const { getAccessToken } = await import("@/lib/auth-utils"); is a dynamic import inside the handler. Move it to a regular top-of-file import.

The biggest issue: this adds 176 lines inline to bounty-detail-sidebar-cta.tsx, growing it from around 387 to 563 lines. Issue #209 has a separate open PR (Biokes) trying to split this file because it's already too big. Please extract a RaiseDisputeDialog component into its own file. The sidebar only needs the trigger button and the disputeDialogOpen state. Move the dialog markup, DISPUTE_REASON_LABELS, validation, mutation wiring, and reset-on-close logic into the new component.

Once those are in this is ready.

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.

Implement the Raise Dispute submission and route to the dispute review page

2 participants