Skip to content

Fraud alert#3706

Merged
steven-tey merged 22 commits into
mainfrom
fraud-alert
Apr 8, 2026
Merged

Fraud alert#3706
steven-tey merged 22 commits into
mainfrom
fraud-alert

Conversation

@devkiran
Copy link
Copy Markdown
Collaborator

@devkiran devkiran commented Apr 7, 2026

Summary by CodeRabbit

  • New Features
    • Fraud Alerts dashboard in the admin panel with filters (status, program), paginated table, and navigation tab.
    • Review workflow: open an alert, add review notes, confirm or dismiss alerts; reviews refresh the list.
    • Flag partners for fraud when banning with optional fraud reason; created fraud alerts are tracked and shown in partner review.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dub Ready Ready Preview Apr 8, 2026 8:39pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 7, 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

Walkthrough

Adds a fraud alert system: DB schema, admin APIs for listing and reviewing alerts, admin UI with filtering/pagination and review sheet, integration with partner ban/unban flows, Zod schemas, and auth handler update to pass session to admin routes.

Changes

Cohort / File(s) Summary
Database Schema
packages/prisma/schema/fraud.prisma, packages/prisma/schema/partner.prisma, packages/prisma/schema/program.prisma, packages/prisma/schema/schema.prisma
Added FraudAlert model and FraudAlertStatus enum; added relations between FraudAlert and Partner/Program/User/ProgramEnrollment; added indexes and relation fields on Partner/Program/ProgramEnrollment/User.
Admin Fraud Alerts UI
apps/web/app/(ee)/admin.dub.co/(dashboard)/fraud-alerts/page.tsx, apps/web/app/(ee)/admin.dub.co/(dashboard)/fraud-alerts/review-fraud-alert-sheet.tsx, apps/web/app/(ee)/admin.dub.co/(dashboard)/layout-nav-client.tsx
New admin page for fraud alerts with SWR fetching, query-driven filters, pagination, table listing, and row -> ReviewFraudAlertSheet flow. Added nav tab entry for Fraud Alerts.
Admin API Routes
apps/web/app/(ee)/api/admin/fraud-alerts/route.ts, apps/web/app/(ee)/api/admin/fraud-alerts/[id]/route.ts, apps/web/app/(ee)/api/admin/partners/[id]/route.ts
Implemented GET list endpoint with pagination/filters; PATCH endpoint to review (dismiss/confirm) alerts with bulk updates and async cross-program ban reporting scheduling; added partner detail GET for review context (payouts, commissions, ban history, prior alerts).
Partner Ban/Unban Integration
apps/web/lib/actions/partners/ban-partner.ts, apps/web/lib/actions/partners/unban-partner.ts, apps/web/app/(ee)/api/partners/ban/route.ts, apps/web/ui/modals/ban-partner-modal.tsx
Extended ban flow to accept flagForFraud and fraudReason (max 500 chars), validate input, create FraudAlert when flagged; ban API and modal updated to surface fields; unban flow deletes pending fraud alerts for partner/program.
Validation & Types
apps/web/lib/zod/schemas/fraud.ts, apps/web/lib/zod/schemas/partners.ts, apps/web/lib/auth/admin.ts
Added fraudAlertSchema Zod schema; extended banPartnerSchema/banPartnerApiSchema and exported MAX_FRAUD_REASON_LENGTH; updated WithAdminHandler to pass session to handlers.

Sequence Diagram(s)

sequenceDiagram
    participant Admin as Admin
    participant Page as Fraud Alerts Page
    participant API as Admin API
    participant DB as Database
    participant Review as Review Sheet

    Admin->>Page: Open fraud alerts page
    Page->>API: GET /api/admin/fraud-alerts?status,programId,page
    API->>DB: Query fraud alerts (+ count) with filters & pagination
    DB-->>API: Return alerts + total
    API-->>Page: Respond with { fraudAlerts, total }
    Page-->>Admin: Render table & pagination

    Admin->>Page: Click alert row
    Page->>Review: Open ReviewFraudAlertSheet(alert)
    Review->>API: GET /api/admin/partners/:partnerId
    API->>DB: Query partner, payouts, commissions, fraudAlerts, enrollments
    DB-->>API: Return partner context
    API-->>Review: Return partner details
    Review-->>Admin: Show context and actions

    Admin->>Review: Submit review (confirm/dismiss + note)
    Review->>API: PATCH /api/admin/fraud-alerts/:id (status, reviewNote)
    API->>DB: Update alert(s) status & review metadata
    API->>DB: (on confirm) Update other pending alerts for partner
    API-->>Review: { success: true }
    Review->>Page: trigger mutate() to refresh list
    Review-->>Admin: Close sheet
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • steven-tey
  • pepeladeira

Poem

🐰✨ I hopped through schemas, APIs, and views,
Flags and reasons in tidy queues.
Admins can review, dismiss, or confirm,
Alerts get tracked — the rabbit affirms! 🥕🎉

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Fraud alert' is vague and generic, lacking specificity about what was implemented. It does not clearly convey the main changes such as the new fraud alerts dashboard, API endpoints, or database schema additions. Consider revising the title to be more specific, such as 'Add fraud alerts admin dashboard with review functionality' or 'Implement fraud alert system with admin review interface'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fraud-alert

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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/lib/actions/partners/ban-partner.ts (1)

50-57: ⚠️ Potential issue | 🟠 Major

API route /api/partners/ban does not forward flagForFraud and fraudReason to the action.

The banPartnerApiSchema includes flagForFraud and fraudReason fields, but the route handler only destructures partnerId, tenantId, and reason from the parsed request body. These fields are not forwarded to banPartner(), preventing API consumers from using the fraud flagging feature.

Update line 14-15 to also destructure flagForFraud and fraudReason, and pass them to the banPartner() call on line 45.

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

In `@apps/web/lib/actions/partners/ban-partner.ts` around lines 50 - 57, The API
route handler that parses the request body (using banPartnerApiSchema) currently
only extracts partnerId, tenantId and reason; update that destructuring to also
extract flagForFraud and fraudReason, and include those two values when invoking
the banPartner(...) action so the call matches BanPartnerInput (i.e., pass
flagForFraud and fraudReason along with partnerId, tenantId/ workspace, reason,
user).
🧹 Nitpick comments (5)
apps/web/lib/zod/schemas/partners.ts (1)

904-915: Encode the flagForFraudfraudReason dependency in the schema.

Right now the conditional requirement is only enforced downstream. Adding it here keeps validation and API contract aligned.

Suggested schema refinement
-export const banPartnerSchema = z.object({
-  workspaceId: z.string(),
-  partnerId: z.string(),
-  reason: z.enum(
-    Object.keys(BAN_PARTNER_REASONS) as [
-      PartnerBannedReason,
-      ...PartnerBannedReason[],
-    ],
-  ),
-  flagForFraud: z.boolean().optional().default(false),
-  fraudReason: z.string().max(MAX_FRAUD_REASON_LENGTH).optional(),
-});
+export const banPartnerSchema = z
+  .object({
+    workspaceId: z.string(),
+    partnerId: z.string(),
+    reason: z.enum(
+      Object.keys(BAN_PARTNER_REASONS) as [
+        PartnerBannedReason,
+        ...PartnerBannedReason[],
+      ],
+    ),
+    flagForFraud: z.boolean().optional().default(false),
+    fraudReason: z.string().max(MAX_FRAUD_REASON_LENGTH).optional(),
+  })
+  .superRefine((data, ctx) => {
+    if (data.flagForFraud && !data.fraudReason?.trim()) {
+      ctx.addIssue({
+        code: "custom",
+        path: ["fraudReason"],
+        message: "Fraud reason is required when flagging for fraud.",
+      });
+    }
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/zod/schemas/partners.ts` around lines 904 - 915, The schema
banPartnerSchema should encode the dependency that fraudReason is required when
flagForFraud is true; update banPartnerSchema to validate this by adding a zod
refinement (e.g., .refine or .superRefine) on the object so that when the parsed
flagForFraud === true the fraudReason (bounded by MAX_FRAUD_REASON_LENGTH) must
be present/non-empty, and when flagForFraud is false it remains optional (or
forbidden) — reference the banPartnerSchema, flagForFraud, fraudReason,
MAX_FRAUD_REASON_LENGTH, and PartnerBannedReason symbols to locate and implement
the change.
apps/web/app/(ee)/admin.dub.co/(dashboard)/fraud-alerts/review-fraud-alert-menu.tsx (1)

39-42: Consider parsing JSON error responses for better error messages.

The API route returns NextResponse.json({ success: true }) on success, but error responses are plain text via new Response(...). The current approach works, but if the API ever returns JSON errors with structured messages, this would display raw JSON.

♻️ Optional: Parse JSON errors when applicable
       if (!response.ok) {
-        const text = await response.text();
-        throw new Error(text);
+        const text = await response.text();
+        let message = text;
+        try {
+          const json = JSON.parse(text);
+          message = json.message || json.error || text;
+        } catch {
+          // Keep original text if not JSON
+        }
+        throw new Error(message);
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/app/`(ee)/admin.dub.co/(dashboard)/fraud-alerts/review-fraud-alert-menu.tsx
around lines 39 - 42, The error handling currently reads response.text() and
throws that raw string when !response.ok; update the block that checks
response.ok (the code using the response variable in
review-fraud-alert-menu.tsx) to attempt parsing JSON first and fall back to
text: try response.json() and if it contains a message or error field include
that in the thrown Error, otherwise use the raw text fallback. Ensure the thrown
Error contains a clear description (e.g., "API error: <message>") so consumers
get structured error info when the API returns JSON errors and still work for
plain-text responses.
apps/web/app/(ee)/admin.dub.co/(dashboard)/fraud-alerts/page.tsx (2)

55-69: Program filter options derived from current page data may be incomplete.

The program filter options are derived from the current page's fraudAlerts data. This means:

  1. On page 2+, programs from page 1 won't appear in the filter
  2. If a filter is already applied, only programs matching that filter appear

Consider fetching all unique programs from a separate endpoint or the first API call without filters for a complete filter experience.

This is acceptable for MVP but may cause confusion when users don't see all programs in the filter dropdown.

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

In `@apps/web/app/`(ee)/admin.dub.co/(dashboard)/fraud-alerts/page.tsx around
lines 55 - 69, The current programs list is computed from the paginated
fraudAlerts inside the useMemo in page.tsx (programs, useMemo, fraudAlerts)
which produces incomplete filter options; fix this by adding a separate data
fetch for the full set of programs (e.g., a new fetchAllPrograms or getPrograms
endpoint call) and populate the programs variable from that result instead of
deriving from fraudAlerts, or call the existing API once without
pagination/filters to retrieve all unique programs; implement the fetch in a
useEffect (or a custom hook) that stores programs in state and use that state in
place of the useMemo so the filter dropdown shows the complete set regardless of
current page or applied fraudAlerts filters.

32-38: Consider adding a fallback to the Suspense boundary.

The Suspense wrapper has no fallback, so the component renders nothing briefly during initial load. While the table handles its own loading state, adding a minimal fallback could improve perceived performance.

♻️ Optional: Add loading skeleton
 export default function FraudAlertsPage() {
   return (
-    <Suspense>
+    <Suspense fallback={<div className="p-6">Loading...</div>}>
       <FraudAlertsPageClient />
     </Suspense>
   );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/`(ee)/admin.dub.co/(dashboard)/fraud-alerts/page.tsx around
lines 32 - 38, The Suspense boundary in FraudAlertsPage wraps
FraudAlertsPageClient without a fallback, causing a blank render during initial
load; update the Suspense usage in FraudAlertsPage to include a minimal fallback
(e.g., a small loading indicator or skeleton) so users see immediate feedback
while FraudAlertsPageClient loads. Locate the Suspense wrapper around
FraudAlertsPageClient in the FraudAlertsPage component and add an appropriate
fallback prop (simple spinner, "Loading..." div, or a lightweight table
skeleton) to improve perceived performance.
apps/web/lib/actions/partners/ban-partner.ts (1)

142-152: Silent failure for fraud alert creation.

The prisma.fraudAlert.create operation is inside Promise.allSettled within waitUntil, meaning any errors will be silently swallowed. If the fraud alert creation fails (e.g., constraint violation), there's no indication to the user or logs.

Consider logging failures for observability, since fraud alerts are important for the review workflow.

🔍 Optional: Add error logging
   waitUntil(
-    Promise.allSettled([
+    Promise.allSettled([
       recordAuditLog({
         ...
       }),
       queue.enqueueJSON({
         ...
       }),
       flagForFraud && fraudReason
-        ? prisma.fraudAlert.create({
-            data: {
-              partnerId,
-              programId,
-              reason: fraudReason,
-            },
-          })
+        ? prisma.fraudAlert.create({
+            data: {
+              partnerId,
+              programId,
+              reason: fraudReason,
+            },
+          }).catch((error) => {
+            console.error("Failed to create fraud alert:", error);
+            throw error;
+          })
         : undefined,
-    ]),
+    ]).then((results) => {
+      results.forEach((result, index) => {
+        if (result.status === "rejected") {
+          console.error(`waitUntil operation ${index} failed:`, result.reason);
+        }
+      });
+    }),
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/actions/partners/ban-partner.ts` around lines 142 - 152, The
fraud alert creation (prisma.fraudAlert.create) is being run inside
Promise.allSettled within waitUntil so any rejection is silently ignored; update
the code to surface failures by either wrapping the fraudAlert.create in its own
try/catch that logs the error (including partnerId, programId and fraudReason)
or by checking the Promise.allSettled result for the fraudAlert entry and
logging any result.status === "rejected" with the rejection reason; reference
the flagForFraud/fraudReason logic, prisma.fraudAlert.create, Promise.allSettled
and waitUntil when making the change and use the project's error logger (e.g.,
logger.error) to record the failure for observability.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/lib/zod/schemas/partners.ts`:
- Around line 917-920: banPartnerApiSchema now includes flagForFraud and
fraudReason but the route handler isn't passing them through; update the route
code where you call banPartner to parse those fields from parseRequestBody using
banPartnerApiSchema (extract partnerId, tenantId, reason, flagForFraud,
fraudReason) and include flagForFraud and fraudReason in the banPartner call so
the fraud flags are propagated to downstream logic; also confirm the banPartner
function signature accepts these fields and add them if missing.

In `@apps/web/ui/modals/ban-partner-modal.tsx`:
- Around line 163-177: When flagForFraud is true the fraudReason field is
registered without client-side validation; update the registration for
fraudReason (the register call in the BanPartnerModal component) to include
validation rules when flagForFraud is enabled (e.g., required: "Reason is
required", maxLength: { value: MAX_FRAUD_REASON_LENGTH, message: "Too long" })
and surface errors.fraudReason in the UI (add conditional error
styling/message). Ensure the conditional logic references flagForFraud and uses
the existing MAX_FRAUD_REASON_LENGTH and errors objects so invalid payloads are
blocked on the client before submit.

In `@packages/prisma/schema/fraud.prisma`:
- Around line 101-104: The FraudAlert model's programEnrollment relation
currently lacks an onDelete policy causing Restrict behavior; update the
relation definition for programEnrollment in the FraudAlert model to include
onDelete: Cascade (i.e., change the ProgramEnrollment relation line to add
onDelete: Cascade) so deletions of ProgramEnrollment cascade to FraudAlert
entries; reference the FraudAlert model and the programEnrollment relation (and
mirror the style used in FraudEventGroup's programEnrollment relation).

---

Outside diff comments:
In `@apps/web/lib/actions/partners/ban-partner.ts`:
- Around line 50-57: The API route handler that parses the request body (using
banPartnerApiSchema) currently only extracts partnerId, tenantId and reason;
update that destructuring to also extract flagForFraud and fraudReason, and
include those two values when invoking the banPartner(...) action so the call
matches BanPartnerInput (i.e., pass flagForFraud and fraudReason along with
partnerId, tenantId/ workspace, reason, user).

---

Nitpick comments:
In `@apps/web/app/`(ee)/admin.dub.co/(dashboard)/fraud-alerts/page.tsx:
- Around line 55-69: The current programs list is computed from the paginated
fraudAlerts inside the useMemo in page.tsx (programs, useMemo, fraudAlerts)
which produces incomplete filter options; fix this by adding a separate data
fetch for the full set of programs (e.g., a new fetchAllPrograms or getPrograms
endpoint call) and populate the programs variable from that result instead of
deriving from fraudAlerts, or call the existing API once without
pagination/filters to retrieve all unique programs; implement the fetch in a
useEffect (or a custom hook) that stores programs in state and use that state in
place of the useMemo so the filter dropdown shows the complete set regardless of
current page or applied fraudAlerts filters.
- Around line 32-38: The Suspense boundary in FraudAlertsPage wraps
FraudAlertsPageClient without a fallback, causing a blank render during initial
load; update the Suspense usage in FraudAlertsPage to include a minimal fallback
(e.g., a small loading indicator or skeleton) so users see immediate feedback
while FraudAlertsPageClient loads. Locate the Suspense wrapper around
FraudAlertsPageClient in the FraudAlertsPage component and add an appropriate
fallback prop (simple spinner, "Loading..." div, or a lightweight table
skeleton) to improve perceived performance.

In
`@apps/web/app/`(ee)/admin.dub.co/(dashboard)/fraud-alerts/review-fraud-alert-menu.tsx:
- Around line 39-42: The error handling currently reads response.text() and
throws that raw string when !response.ok; update the block that checks
response.ok (the code using the response variable in
review-fraud-alert-menu.tsx) to attempt parsing JSON first and fall back to
text: try response.json() and if it contains a message or error field include
that in the thrown Error, otherwise use the raw text fallback. Ensure the thrown
Error contains a clear description (e.g., "API error: <message>") so consumers
get structured error info when the API returns JSON errors and still work for
plain-text responses.

In `@apps/web/lib/actions/partners/ban-partner.ts`:
- Around line 142-152: The fraud alert creation (prisma.fraudAlert.create) is
being run inside Promise.allSettled within waitUntil so any rejection is
silently ignored; update the code to surface failures by either wrapping the
fraudAlert.create in its own try/catch that logs the error (including partnerId,
programId and fraudReason) or by checking the Promise.allSettled result for the
fraudAlert entry and logging any result.status === "rejected" with the rejection
reason; reference the flagForFraud/fraudReason logic, prisma.fraudAlert.create,
Promise.allSettled and waitUntil when making the change and use the project's
error logger (e.g., logger.error) to record the failure for observability.

In `@apps/web/lib/zod/schemas/partners.ts`:
- Around line 904-915: The schema banPartnerSchema should encode the dependency
that fraudReason is required when flagForFraud is true; update banPartnerSchema
to validate this by adding a zod refinement (e.g., .refine or .superRefine) on
the object so that when the parsed flagForFraud === true the fraudReason
(bounded by MAX_FRAUD_REASON_LENGTH) must be present/non-empty, and when
flagForFraud is false it remains optional (or forbidden) — reference the
banPartnerSchema, flagForFraud, fraudReason, MAX_FRAUD_REASON_LENGTH, and
PartnerBannedReason symbols to locate and implement the change.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 22acc688-2455-4374-9985-10137f58c4f9

📥 Commits

Reviewing files that changed from the base of the PR and between 2ddaf9f and 883e38a.

📒 Files selected for processing (14)
  • apps/web/app/(ee)/admin.dub.co/(dashboard)/fraud-alerts/page.tsx
  • apps/web/app/(ee)/admin.dub.co/(dashboard)/fraud-alerts/review-fraud-alert-menu.tsx
  • apps/web/app/(ee)/admin.dub.co/(dashboard)/layout-nav-client.tsx
  • apps/web/app/(ee)/api/admin/fraud-alerts/[id]/route.ts
  • apps/web/app/(ee)/api/admin/fraud-alerts/route.ts
  • apps/web/lib/actions/partners/ban-partner.ts
  • apps/web/lib/auth/admin.ts
  • apps/web/lib/zod/schemas/fraud.ts
  • apps/web/lib/zod/schemas/partners.ts
  • apps/web/ui/modals/ban-partner-modal.tsx
  • packages/prisma/schema/fraud.prisma
  • packages/prisma/schema/partner.prisma
  • packages/prisma/schema/program.prisma
  • packages/prisma/schema/schema.prisma

Comment thread apps/web/lib/zod/schemas/partners.ts
Comment thread apps/web/ui/modals/ban-partner-modal.tsx Outdated
Comment thread packages/prisma/schema/fraud.prisma Outdated
@devkiran devkiran requested a review from steven-tey April 7, 2026 10:28
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)
apps/web/app/(ee)/api/admin/fraud-alerts/[id]/route.ts (1)

22-24: Unhandled Zod validation error returns 500 instead of 400.

reviewSchema.parse() throws a ZodError on invalid input, which will result in a 500 response. Consider using safeParse or wrapping in try-catch to return a proper 400 response with validation details.

♻️ Proposed fix using safeParse
-  const { status: newStatus, reviewNote } = reviewSchema.parse(
-    await req.json(),
-  );
+  const body = await req.json();
+  const parsed = reviewSchema.safeParse(body);
+  
+  if (!parsed.success) {
+    return new Response("Invalid request body.", { status: 400 });
+  }
+  
+  const { status: newStatus, reviewNote } = parsed.data;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/`(ee)/api/admin/fraud-alerts/[id]/route.ts around lines 22 - 24,
The current call reviewSchema.parse(await req.json()) can throw a ZodError and
produce a 500; change it to use reviewSchema.safeParse(await req.json()) (or
wrap parse in try/catch) and if validation fails return a 400 with the
validation error details; update the code that reads const { status: newStatus,
reviewNote } to only destructure after a successful safeParse (e.g., from
result.data) and ensure the response for failures includes the Zod error
messages.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/web/app/`(ee)/api/admin/fraud-alerts/[id]/route.ts:
- Around line 22-24: The current call reviewSchema.parse(await req.json()) can
throw a ZodError and produce a 500; change it to use
reviewSchema.safeParse(await req.json()) (or wrap parse in try/catch) and if
validation fails return a 400 with the validation error details; update the code
that reads const { status: newStatus, reviewNote } to only destructure after a
successful safeParse (e.g., from result.data) and ensure the response for
failures includes the Zod error messages.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ed167989-e8c4-4726-aba1-cc79230621a6

📥 Commits

Reviewing files that changed from the base of the PR and between 883e38a and 8cd63a2.

📒 Files selected for processing (7)
  • apps/web/app/(ee)/admin.dub.co/(dashboard)/fraud-alerts/review-fraud-alert-menu.tsx
  • apps/web/app/(ee)/api/admin/fraud-alerts/[id]/route.ts
  • apps/web/app/(ee)/api/partners/ban/route.ts
  • apps/web/lib/actions/partners/unban-partner.ts
  • apps/web/lib/zod/schemas/partners.ts
  • apps/web/ui/modals/ban-partner-modal.tsx
  • packages/prisma/schema/fraud.prisma
✅ Files skipped from review due to trivial changes (1)
  • apps/web/ui/modals/ban-partner-modal.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/web/lib/zod/schemas/partners.ts
  • apps/web/app/(ee)/admin.dub.co/(dashboard)/fraud-alerts/review-fraud-alert-menu.tsx
  • packages/prisma/schema/fraud.prisma

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: 4

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

Inline comments:
In `@apps/web/app/`(ee)/admin.dub.co/(dashboard)/fraud-alerts/page.tsx:
- Around line 58-72: The programs list is being built from the paginated
fraudAlerts (useMemo computing programs from fraudAlerts), which makes the
dropdown only show programs on the current page; instead fetch or accept a
dedicated full-programs source and use that for the filter options. Replace the
current useMemo that derives programs from fraudAlerts with a call/prop to the
global programs list (e.g., usePrograms(), getAllPrograms(), or a programs prop
returned alongside the paginated alerts) and update any other similar
derivations in the block around the useMemo and lines referenced (the programs
variable and related filter-building code in the same component) to consume that
dedicated source so options are stable regardless of paging or current filters.

In `@apps/web/app/`(ee)/api/admin/fraud-alerts/[id]/route.ts:
- Around line 40-45: The current read-then-write flow in the confirm/reject
paths (in route.ts handling the fraud alert) can race; change to a single
guarded write inside a transaction so the update only succeeds if
fraudAlert.status === "pending" (e.g., use your ORM/DB transaction API to
perform the reporting findMany and the status update together, applying a
WHERE/status guard on the update). If the guarded update returns 0 rows
affected, return HTTP 409. Apply the same guarded transactional update for both
the confirm and reject handlers (the functions/blocks that currently check
fraudAlert.status !== "pending" and then update the record and audit fields) so
audit fields and enqueued reports are consistent.

In `@apps/web/app/`(ee)/api/admin/partners/[id]/route.ts:
- Around line 9-16: The route currently returns the full Partner row via
prisma.partner.findUnique and assigns it to partner, leaking sensitive
payout/identity fields; change the call to use an explicit select that only
fetches the fields the review sheet renders (e.g., id, name, email, status, any
display fields) and for related platforms use include or nested select to return
only platform fields needed; update both occurrences where
prisma.partner.findUnique (and the partner response usage around the other
occurrence mentioned) are used so the response object contains only those
selected fields before sending to the client.
- Around line 100-103: The platforms mapping currently only converts subscribers
and turns 0n into null and leaves posts and views as BigInt, causing JSON
serialization errors; update the partner.platforms mapping (the platforms:
partner.platforms.map((p) => ... ) block) to consistently serialize BigInt
fields by converting subscribers, posts, and views to strings when present
(e.g., if p.subscribers != null then p.subscribers.toString(), otherwise null)
so 0n becomes "0" and posts/views are also safe for JSON.stringify.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 769e18d8-8fdd-496c-b8ef-d21fd404793a

📥 Commits

Reviewing files that changed from the base of the PR and between b090b58 and 40bf168.

📒 Files selected for processing (5)
  • apps/web/app/(ee)/admin.dub.co/(dashboard)/fraud-alerts/page.tsx
  • apps/web/app/(ee)/admin.dub.co/(dashboard)/fraud-alerts/review-fraud-alert-sheet.tsx
  • apps/web/app/(ee)/admin.dub.co/(dashboard)/layout-nav-client.tsx
  • apps/web/app/(ee)/api/admin/fraud-alerts/[id]/route.ts
  • apps/web/app/(ee)/api/admin/partners/[id]/route.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/web/app/(ee)/admin.dub.co/(dashboard)/layout-nav-client.tsx

Comment thread apps/web/app/(ee)/admin.dub.co/(dashboard)/fraud-alerts/page.tsx
Comment thread apps/web/app/(ee)/api/admin/fraud-alerts/[id]/route.ts Outdated
Comment thread apps/web/app/(ee)/api/admin/partners/[id]/route.ts
Comment thread apps/web/app/(ee)/api/admin/partners/[id]/route.ts
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.

♻️ Duplicate comments (1)
apps/web/app/(ee)/api/admin/partners/[id]/route.ts (1)

107-110: ⚠️ Potential issue | 🔴 Critical

BigInt serialization incomplete—posts and views will crash JSON.stringify.

The previous review flagged this: only subscribers is converted, but posts and views from the platforms relation are also BigInt columns. NextResponse.json() will throw at runtime when these fields are present.

🐛 Proposed fix to serialize all BigInt fields
     platforms: partner.platforms.map((p) => ({
       ...p,
-      subscribers: p.subscribers ? Number(p.subscribers) : null,
+      subscribers: p.subscribers != null ? p.subscribers.toString() : null,
+      posts: p.posts != null ? p.posts.toString() : null,
+      views: p.views != null ? p.views.toString() : null,
     })),

Using .toString() preserves precision for potentially large values that could exceed Number.MAX_SAFE_INTEGER.

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

In `@apps/web/app/`(ee)/api/admin/partners/[id]/route.ts around lines 107 - 110,
The platforms mapping currently only converts subscribers to a Number, but posts
and views are also BigInt and will break JSON serialization; update the mapping
(the platforms: partner.platforms.map(...) block) to convert posts and views to
string (e.g., p.posts ? p.posts.toString() : null and same for p.views) while
preserving the existing handling for subscribers so NextResponse.json() can
safely serialize the object.
🧹 Nitpick comments (1)
apps/web/app/(ee)/api/admin/partners/[id]/route.ts (1)

68-102: Consider adding pagination/limits to unbounded queries.

The payouts and commissions queries have no take limit. For partners with extensive history, this could return very large datasets impacting response time and memory.

Additionally, the commissions query returns all fields via include, but the consumer (PartnerDetail type) only needs id, earnings, currency, status, createdAt, and program. Consider using select to reduce payload size.

♻️ Suggested improvement
       prisma.commission.findMany({
         where: {
           partnerId: id,
         },
-        include: {
-          program: {
-            select: {
-              id: true,
-              name: true,
-              logo: true,
+        select: {
+          id: true,
+          earnings: true,
+          currency: true,
+          status: true,
+          createdAt: true,
+          program: {
+            select: {
+              id: true,
+              name: true,
+              logo: true,
+            },
           },
         },
         orderBy: {
           createdAt: "desc",
         },
+        take: 100,
       }),

Apply similar take limit to payouts query as well.

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

In `@apps/web/app/`(ee)/api/admin/partners/[id]/route.ts around lines 68 - 102,
The payouts and commissions queries (prisma.payout.findMany and
prisma.commission.findMany) are unbounded and the commissions include all fields
unnecessarily for PartnerDetail; add a sensible limit (e.g., take: 50 or page
params) to both queries and replace the commissions include with a select that
returns only id, earnings, currency, status, createdAt and program (program: {
select: { id, name, logo } }) so the payload is bounded and matches the
PartnerDetail shape; if pagination is intended, wire in skip/take or cursor
params instead of a hard limit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/web/app/`(ee)/api/admin/partners/[id]/route.ts:
- Around line 107-110: The platforms mapping currently only converts subscribers
to a Number, but posts and views are also BigInt and will break JSON
serialization; update the mapping (the platforms: partner.platforms.map(...)
block) to convert posts and views to string (e.g., p.posts ? p.posts.toString()
: null and same for p.views) while preserving the existing handling for
subscribers so NextResponse.json() can safely serialize the object.

---

Nitpick comments:
In `@apps/web/app/`(ee)/api/admin/partners/[id]/route.ts:
- Around line 68-102: The payouts and commissions queries
(prisma.payout.findMany and prisma.commission.findMany) are unbounded and the
commissions include all fields unnecessarily for PartnerDetail; add a sensible
limit (e.g., take: 50 or page params) to both queries and replace the
commissions include with a select that returns only id, earnings, currency,
status, createdAt and program (program: { select: { id, name, logo } }) so the
payload is bounded and matches the PartnerDetail shape; if pagination is
intended, wire in skip/take or cursor params instead of a hard limit.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5bd05a2e-fe83-48ac-a4af-44bd021361a8

📥 Commits

Reviewing files that changed from the base of the PR and between 40bf168 and a986916.

📒 Files selected for processing (2)
  • apps/web/app/(ee)/api/admin/fraud-alerts/[id]/route.ts
  • apps/web/app/(ee)/api/admin/partners/[id]/route.ts

@steven-tey steven-tey merged commit e8cf97f into main Apr 8, 2026
9 checks passed
@steven-tey steven-tey deleted the fraud-alert branch April 8, 2026 20:46
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.

2 participants