Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@devkiran
Copy link
Collaborator

@devkiran devkiran commented Aug 16, 2025

Summary by CodeRabbit

  • New Features
    • Export commissions to CSV from the commissions popover via an "Export as CSV" action.
    • Modal to select columns (defaults provided) with validation and option to apply current filters (date range, status, partner, invoice, etc.).
    • Download produces a timestamped, filesystem-safe CSV (records limited for performance), with clear loading, success, and error toasts.

@vercel
Copy link
Contributor

vercel bot commented Aug 16, 2025

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

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Aug 16, 2025 5:39pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 16, 2025

Walkthrough

Adds a commissions CSV export feature: a new GET API route that validates export query and columns, a modal UI to select columns and include current filters, and popover wiring to open the modal and trigger the export download.

Changes

Cohort / File(s) Summary of changes
API: Commissions CSV Export
apps/web/app/(ee)/api/commissions/export/route.ts
New GET route that parses and validates search params (columns, filters, dates, sorting) via Zod, resolves workspace/program, queries Prisma (invoiceId OR date window + filters, includes customer & partner), maps rows, validates per-column types with dynamic Zod schemas, converts to CSV and returns downloadable response with sanitized timestamp filename.
Validation & Config
apps/web/lib/zod/schemas/commissions.ts
Adds COMMISSION_EXPORT_COLUMNS, DEFAULT_COMMISSION_EXPORT_COLUMNS, and commissionsExportQuerySchema (extends existing query schema without pagination). Normalizes and validates columns param against allowed column IDs and provides defaults.
UI: Export Modal
apps/web/ui/modals/export-commissions-modal.tsx
Adds useExportCommissionsModal hook and ExportCommissionsModal component using react-hook-form: columns checkbox grid, "apply current filters" option, builds query string, performs GET to export endpoint, handles blob download, toasts, and modal visibility API.
UI Wiring: Popover Action
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-popover-buttons.tsx
Renders ExportCommissionsModal, adds "Export as CSV" action (Download icon) that closes the popover and opens the modal via the hook's setter.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant UI as Commissions Page
  participant Pop as Popover
  participant Modal as ExportCommissionsModal
  participant API as GET /api/commissions/export
  participant DB as Prisma

  User->>UI: Open commissions popover
  User->>Pop: Click "Export as CSV"
  Pop->>Modal: setShowExportCommissionsModal(true)
  User->>Modal: Select columns / apply filters, Submit
  Modal->>API: GET with workspaceId, columns, filters
  API->>DB: Query commissions (invoiceId OR date window + filters)
  DB-->>API: Result set (≤1000)
  API->>API: Validate & map rows, build CSV
  API-->>Modal: CSV response (blob)
  Modal->>User: Trigger CSV download
Loading
sequenceDiagram
  participant Client
  participant Zod as commissionsExportQuerySchema
  participant Prisma
  participant Formatter as CSV Builder
  participant FS as Response

  Client->>Zod: Send searchParams (columns, filters)
  Zod-->>Client: Validated params
  Client->>Prisma: Build & run query
  Prisma-->>Client: Rows
  Client->>Formatter: Map rows, apply per-column schemas, serialize CSV
  Formatter-->>FS: Return CSV stream
  FS-->>Client: Respond with text/csv + Content-Disposition
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Create clawback #2525 — Overlaps on commissions filtering and schema changes (earnings filter / commissions schema adjustments).

Suggested reviewers

  • TWilson023

Poem

A rabbit taps keys by soft lamplight,
Columns chosen, checkboxes bright.
Filters set, timestamp snug and sweet,
CSV hops out — a downloadable treat! 🥕📄

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch export-commissions

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@devkiran devkiran marked this pull request as ready for review August 16, 2025 11:16
Copy link
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 (11)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-popover-buttons.tsx (1)

43-45: Align title casing with surrounding UI (nit)

Elsewhere (e.g., the modal header at Line 111 in export-commissions-modal.tsx), “Export commissions” uses sentence case. Consider matching here for consistency.

-                Export Commissions
+                Export commissions
apps/web/app/(ee)/api/commissions/export/route.ts (5)

70-110: Prefer Date objects over ISO strings for temporal filters

Passing toISOString is unnecessary and can introduce subtle timezone edge cases. Prisma accepts Date objects directly.

-          createdAt: {
-            gte: startDate.toISOString(),
-            lte: endDate.toISOString(),
-          },
+          createdAt: {
+            gte: startDate,
+            lte: endDate,
+          },

27-40: Simplify and harden type transforms for CSV shaping

  • number: using value || 0 conflates 0 and NaN/null; prefer nullish coalescing.
  • date: since inputs are Date, coercion isn’t needed; ?. is redundant.
  • string: fine, but you can avoid extra default by using nullish coalescing.
 const COLUMN_TYPE_SCHEMAS = {
-  number: z.coerce
-    .number()
-    .nullable()
-    .default(0)
-    .transform((value) => value || 0),
-  date: z.date().transform((date) => date?.toISOString() || ""),
-  string: z
-    .string()
-    .nullable()
-    .default("")
-    .transform((value) => value || ""),
+  number: z.number().nullable().transform((value) => value ?? 0),
+  date: z.date().transform((date) => date.toISOString()),
+  string: z.string().nullable().transform((value) => value ?? ""),
 };

141-144: Use human-friendly column labels and preserve header order even for empty datasets

Currently headers will be column IDs. If you want labels in the CSV, map keys to labels in the selected order before conversion. This also ensures headers are present even when no rows match.

-  const data = z.array(z.object(columnSchemas)).parse(formattedCommissions);
-  const csvData = convertToCSV(data);
+  const data = z.array(z.object(columnSchemas)).parse(formattedCommissions);
+  const labeledData = data.map((row) => {
+    const out: Record<string, unknown> = {};
+    for (const id of columns) {
+      const label = COLUMN_LOOKUP.get(id)?.label ?? id;
+      // @ts-expect-error index access at runtime
+      out[label] = row[id];
+    }
+    return out;
+  });
+  const csvData = convertToCSV(labeledData);

If convertToCSV already supports a header mapping argument, prefer using that instead of re-keying objects.


43-43: Confirm permissions/plan gating for export endpoint

If CSV export should be restricted (e.g., to specific roles or plans), pass options to withWorkspace to enforce it. Otherwise, this endpoint inherits only the base workspace checks.

Example (adjust for your permission constants):

export const GET = withWorkspace(
  async ({ searchParams, workspace }) => { /* ... */ },
  { requiredPermissions: ["commissions:read"] /*, requiredPlan: ["pro","enterprise"]*/ },
);

106-109: Hard cap of 1000 rows—verify product intent

The API silently truncates to 1000 rows. If that’s intentional, consider reflecting it in the UI copy or providing pagination/chunked exporting later.

apps/web/lib/zod/schemas/commissions.ts (1)

255-279: Trim and dedupe incoming columns for resilience

Users or clients could send space-delimited/duplicate IDs. Normalize the array before validation.

 export const commissionsExportQuerySchema = getCommissionsQuerySchema
   .omit({ page: true, pageSize: true })
   .merge(
     z.object({
       columns: z
         .string()
-        .default(DEFAULT_COMMISSION_EXPORT_COLUMNS.join(","))
-        .transform((v) => v.split(","))
+        .default(DEFAULT_COMMISSION_EXPORT_COLUMNS.join(","))
+        .transform((v) =>
+          Array.from(
+            new Set(
+              v
+                .split(",")
+                .map((s) => s.trim())
+                .filter(Boolean),
+            ),
+          ),
+        )
         .refine(
           (columns): columns is CommissionExportColumnId[] => {
             const validColumnIds = COMMISSION_EXPORT_COLUMNS.map(
               (col) => col.id,
             );
apps/web/ui/modals/export-commissions-modal.tsx (4)

55-59: Provide user feedback when context is missing

If workspace or program isn’t resolved, return with a toast so users know why nothing happened.

   const onSubmit = handleSubmit(async (data) => {
     if (!workspaceId || !program?.id) {
-      return;
+      toast.error("Unable to export: missing workspace or program context.");
+      return;
     }

74-80: Avoid unnecessary headers on GET

Content-Type isn’t needed for GET and may trigger CORS preflight in some setups.

-      const response = await fetch(`/api/commissions/export${searchParams}`, {
-        method: "GET",
-        headers: {
-          "Content-Type": "application/json",
-        },
-      });
+      const response = await fetch(`/api/commissions/export${searchParams}`, {
+        method: "GET",
+      });

81-85: Surface clean error messages in toasts

Passing an Error object to sonner will render “[object Object]”. Use the message string.

-      if (!response.ok) {
-        const { error } = await response.json();
-        throw new Error(error.message);
-      }
+      if (!response.ok) {
+        const { error } = await response.json().catch(() => ({}));
+        throw new Error(error?.message || "Failed to export commissions.");
+      }
@@
-    } catch (error) {
-      toast.error(error);
+    } catch (error) {
+      toast.error(error instanceof Error ? error.message : String(error));

Also applies to: 96-101


86-93: Use server-suggested filename and make it filesystem-safe; revoke object URL

  • Parse Content-Disposition for a server-suggested filename (keeps API/UI in sync).
  • Sanitize timestamp fallback to avoid colons.
  • Revoke the URL to free memory.
-      const blob = await response.blob();
-      const url = window.URL.createObjectURL(blob);
-      const a = document.createElement("a");
-
-      a.href = url;
-      a.download = `Dub Commissions Export - ${new Date().toISOString()}.csv`;
-      a.click();
+      const blob = await response.blob();
+      const url = window.URL.createObjectURL(blob);
+      const a = document.createElement("a");
+      const disposition = response.headers.get("content-disposition");
+      const suggestedFilename = disposition?.match(/filename="(.+?)"/)?.[1];
+      const timestamp = new Date().toISOString().replace(/:/g, "-");
+      const filename =
+        suggestedFilename || `Dub Commissions Export - ${timestamp}.csv`;
+      a.href = url;
+      a.download = filename;
+      a.click();
+      setTimeout(() => URL.revokeObjectURL(url), 0);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f4b9778 and e918f75.

📒 Files selected for processing (4)
  • apps/web/app/(ee)/api/commissions/export/route.ts (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-popover-buttons.tsx (3 hunks)
  • apps/web/lib/zod/schemas/commissions.ts (1 hunks)
  • apps/web/ui/modals/export-commissions-modal.tsx (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
apps/web/app/(ee)/api/commissions/export/route.ts (2)
apps/web/lib/zod/schemas/commissions.ts (3)
  • COMMISSION_EXPORT_COLUMNS (213-245)
  • commissionsExportQuerySchema (255-279)
  • getCommissionsQuerySchema (44-98)
apps/web/lib/auth/workspace.ts (1)
  • withWorkspace (41-435)
apps/web/ui/modals/export-commissions-modal.tsx (4)
apps/web/lib/swr/use-workspace.ts (1)
  • useWorkspace (6-45)
apps/web/lib/zod/schemas/commissions.ts (2)
  • DEFAULT_COMMISSION_EXPORT_COLUMNS (250-253)
  • COMMISSION_EXPORT_COLUMNS (213-245)
apps/web/lib/openapi/index.ts (1)
  • document (23-83)
packages/ui/src/tooltip.tsx (1)
  • InfoTooltip (193-199)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-popover-buttons.tsx (1)
apps/web/ui/modals/export-commissions-modal.tsx (1)
  • useExportCommissionsModal (177-197)
🔇 Additional comments (6)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-popover-buttons.tsx (2)

15-17: Clean integration of the export modal hook

The modal is correctly instantiated once and mounted in-tree; the setter wiring is sound.

Also applies to: 21-21


42-58: Popover action correctly opens the export modal

Closing the popover before opening the modal prevents overlay conflicts. The UI pattern matches the existing “Create clawback” action.

apps/web/lib/zod/schemas/commissions.ts (1)

213-245: Well-structured, typed column catalog

Clear ids, labels, and types with defaults will make the UI and server code robust and consistent.

apps/web/ui/modals/export-commissions-modal.tsx (3)

118-154: Columns controller and checkbox grid look good

State updates are minimal and correctly derived; IDs are properly associated with labels for accessibility.


171-171: Button loading state integration is correct

The submit button correctly reflects isSubmitting state.


177-196: Stable modal hook API

The memoized component wrapper and setter export make consumption simple and avoid unnecessary remounts.

@devkiran
Copy link
Collaborator Author

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 16, 2025

✅ Actions performed

Full review triggered.

Copy link
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: 0

♻️ Duplicate comments (2)
apps/web/app/(ee)/api/commissions/export/route.ts (2)

92-101: Group filter is now applied in the export query (resolved)

The conditional partner group filter is included via partner.programs.some({ programId, groupId }). This addresses the earlier omission.


156-163: Filesystem-safe filename handled (resolved)

Replacing colons in the ISO timestamp fixes Windows filename issues. Thanks for addressing this.

🧹 Nitpick comments (9)
apps/web/lib/zod/schemas/commissions.ts (2)

259-277: Harden columns parsing: trim, dedupe, and return precise validation errors

Current parsing accepts whitespace and duplicates and returns a generic error. Tighten it to:

  • trim each id
  • drop empties
  • dedupe
  • surface invalid ids in the error message

Apply this diff:

-      columns: z
-        .string()
-        .default(DEFAULT_COMMISSION_EXPORT_COLUMNS.join(","))
-        .transform((v) => v.split(","))
-        .refine(
-          (columns): columns is CommissionExportColumnId[] => {
-            const validColumnIds = COMMISSION_EXPORT_COLUMNS.map(
-              (col) => col.id,
-            );
-
-            return columns.every((column): column is CommissionExportColumnId =>
-              validColumnIds.includes(column as CommissionExportColumnId),
-            );
-          },
-          {
-            message:
-              "Invalid column IDs provided. Please check the available columns.",
-          },
-        ),
+      columns: z
+        .string()
+        .default(DEFAULT_COMMISSION_EXPORT_COLUMNS.join(","))
+        .transform((v) =>
+          Array.from(
+            new Set(
+              v
+                .split(",")
+                .map((s) => s.trim())
+                .filter(Boolean),
+            ),
+          ),
+        )
+        .superRefine((columns, ctx) => {
+          const validIds = new Set(
+            COMMISSION_EXPORT_COLUMNS.map((col) => col.id),
+          );
+          const invalid = columns.filter((c) => !validIds.has(c));
+          if (invalid.length) {
+            ctx.addIssue({
+              code: z.ZodIssueCode.custom,
+              message: `Invalid column IDs: ${invalid.join(", ")}`,
+            });
+          }
+        }),

247-253: Export the column id type and type the defaults

Exporting the id type improves type-safety in consumers (UI, route) and typing the defaults prevents accidental drift.

-type CommissionExportColumnId =
+export type CommissionExportColumnId =
   (typeof COMMISSION_EXPORT_COLUMNS)[number]["id"];
 
-export const DEFAULT_COMMISSION_EXPORT_COLUMNS =
+export const DEFAULT_COMMISSION_EXPORT_COLUMNS: CommissionExportColumnId[] =
   COMMISSION_EXPORT_COLUMNS.filter((column) => column.default).map(
     (column) => column.id,
   );
apps/web/app/(ee)/api/commissions/export/route.ts (2)

89-91: Prefer passing Date objects to Prisma instead of ISO strings

Prisma accepts Date objects directly. Avoiding toISOString keeps types consistent and eliminates needless string conversion.

-          createdAt: {
-            gte: startDate.toISOString(),
-            lte: endDate.toISOString(),
-          },
+          createdAt: {
+            gte: startDate,
+            lte: endDate,
+          },

13-25: Tighten typing for column types to prevent mismatches

Map value currently uses type: string, which weakens type-safety when indexing COLUMN_TYPE_SCHEMAS. Constrain it to the known keys.

-const COLUMN_LOOKUP: Map<
-  string,
-  { label: string; type: string; order: number }
-> = new Map(
+type ColumnType = keyof typeof COLUMN_TYPE_SCHEMAS;
+const COLUMN_LOOKUP: Map<
+  string,
+  { label: string; type: ColumnType; order: number }
+> = new Map(
apps/web/ui/modals/export-commissions-modal.tsx (5)

91-93: Sanitize the download filename and revoke the blob URL

Colons break Windows filenames and the blob URL should be revoked to avoid leaks. The server already sets a safe filename; keep it consistent client-side.

-      a.download = `Dub Commissions Export - ${new Date().toISOString()}.csv`;
-      a.click();
+      a.download = `Dub Commissions Export - ${new Date()
+        .toISOString()
+        .replace(/:/g, "-")}.csv`;
+      a.click();
+      a.remove();
+      // Revoke on next tick to avoid interfering with download
+      setTimeout(() => URL.revokeObjectURL(url), 0);

76-79: Drop the Content-Type header on GET

Unnecessary for a GET and can be omitted.

-      const response = await fetch(`/api/commissions/export${searchParams}`, {
-        method: "GET",
-        headers: {
-          "Content-Type": "application/json",
-        },
-      });
+      const response = await fetch(`/api/commissions/export${searchParams}`, {
+        method: "GET",
+      });

63-69: Guard against exporting with zero columns (better UX)

If no columns are selected, the server will 400. Short-circuit with a clear message.

       const params = {
         workspaceId,
         ...(data.columns.length
           ? { columns: data.columns.join(",") }
           : undefined),
       };
 
+      if (data.columns.length === 0) {
+        throw new Error("Select at least one column to export.");
+      }

96-100: Ensure toast displays a useful message for unknown errors

toast.error(error) may render [object Object]. Use a string message fallback.

-    } catch (error) {
-      toast.error(error);
+    } catch (error) {
+      const message =
+        error instanceof Error
+          ? error.message
+          : "Failed to export commissions. Please try again.";
+      toast.error(message);
     } finally {
       toast.dismiss(lid);
     }

55-58: Handle missing context explicitly

If context is missing, surface a user-facing error instead of silently returning.

-    if (!workspaceId || !program?.id) {
-      return;
-    }
+    if (!workspaceId || !program?.id) {
+      toast.error("Missing workspace or program context.");
+      return;
+    }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f4b9778 and 1424dd5.

📒 Files selected for processing (4)
  • apps/web/app/(ee)/api/commissions/export/route.ts (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-popover-buttons.tsx (3 hunks)
  • apps/web/lib/zod/schemas/commissions.ts (1 hunks)
  • apps/web/ui/modals/export-commissions-modal.tsx (1 hunks)
🔇 Additional comments (3)
apps/web/lib/zod/schemas/commissions.ts (1)

213-245: Column config looks comprehensive and consistent

Good coverage of core commission, partner, and customer fields, with sensible defaults and types.

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-popover-buttons.tsx (1)

15-22: Clean integration of Export modal into the popover

Hook wiring and rendering look good; UX flows correctly from popover → modal.

apps/web/app/(ee)/api/commissions/export/route.ts (1)

117-121: Verify the 1,000-row cap in commissions export

The commissions export endpoint (apps/web/app/(ee)/api/commissions/export/route.ts:117) is hard-capped at take: 1000. Please confirm this limit matches product expectations. If users require full exports, consider one or more of the following:

  • Raising the cap (e.g., to 10 000)
  • Implementing chunked or streaming exports
  • Exposing pagination (page/token) to fetch additional rows

Note that other list endpoints in apps/web/app default to much smaller limits (50–100 rows), so ensure any new cap remains consistent across APIs.

@steven-tey
Copy link
Collaborator

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 16, 2025

✅ Actions performed

Full review triggered.

Copy link
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

♻️ Duplicate comments (2)
apps/web/app/(ee)/api/commissions/export/route.ts (2)

92-101: Resolved: groupId filter now applied in the query

The conditional partner.programs some { programId, groupId } fixes the earlier omission.


167-175: Filename now sanitized for Windows compatibility

Good fix replacing colons in the ISO timestamp before using it in Content-Disposition.

🧹 Nitpick comments (6)
apps/web/lib/zod/schemas/commissions.ts (1)

267-291: Deduplicate and sanitize column IDs at parse-time

Avoid duplicates and empty IDs from malformed queries (e.g., trailing commas) by filtering empties and deduping after split.

Apply this diff:

   export const commissionsExportQuerySchema = getCommissionsQuerySchema
     .omit({ page: true, pageSize: true })
     .merge(
       z.object({
         columns: z
           .string()
           .default(DEFAULT_COMMISSION_EXPORT_COLUMNS.join(","))
-          .transform((v) => v.split(","))
+          .transform((v) => v.split(",").filter(Boolean))
+          .transform((arr) => Array.from(new Set(arr)))
           .refine(
             (columns): columns is CommissionExportColumnId[] => {
               const validColumnIds = COMMISSION_EXPORT_COLUMNS.map(
                 (col) => col.id,
               );

               return columns.every((column): column is CommissionExportColumnId =>
                 validColumnIds.includes(column as CommissionExportColumnId),
               );
             },
             {
               message:
                 "Invalid column IDs provided. Please check the available columns.",
             },
           ),
       }),
     );
apps/web/app/(ee)/api/commissions/export/route.ts (2)

88-91: Prefer Date objects over ISO strings in Prisma filters

Prisma accepts Date objects directly; passing Dates avoids any potential timezone/string parsing edge cases.

Apply this diff:

           createdAt: {
-            gte: startDate.toISOString(),
-            lte: endDate.toISOString(),
+            gte: startDate,
+            lte: endDate,
           },

144-148: Guard against duplicate columns before sorting

If duplicates sneak in, the CSV would repeat columns. Dedup here as a final safeguard.

Apply this diff:

-  columns = columns.sort(
+  columns = [...new Set(columns)].sort(
     (a, b) =>
       (COLUMN_LOOKUP.get(a)?.order || 999) -
       (COLUMN_LOOKUP.get(b)?.order || 999),
   );
apps/web/ui/modals/export-commissions-modal.tsx (3)

74-79: Remove unnecessary Content-Type header on GET

No body is sent; some intermediaries treat GET+Content-Type oddly.

Apply this diff:

-      const response = await fetch(`/api/commissions/export${searchParams}`, {
-        method: "GET",
-        headers: {
-          "Content-Type": "application/json",
-        },
-      });
+      const response = await fetch(`/api/commissions/export${searchParams}`);

97-100: Surface the actual error message in toast

Passing the Error object directly can render as [object Object].

Apply this diff:

-    } catch (error) {
-      toast.error(error);
+    } catch (error) {
+      toast.error(error instanceof Error ? error.message : String(error));

123-155: Optional: prevent submitting with zero columns selected

If a user unchecks everything, the backend falls back to defaults, which might be surprising. Consider disabling submit or showing a validation error when no columns are selected.

I can add a small client-side guard to disable the submit button when columns.length === 0, if desired.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f4b9778 and ec296de.

📒 Files selected for processing (4)
  • apps/web/app/(ee)/api/commissions/export/route.ts (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-popover-buttons.tsx (3 hunks)
  • apps/web/lib/zod/schemas/commissions.ts (1 hunks)
  • apps/web/ui/modals/export-commissions-modal.tsx (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-popover-buttons.tsx (1)
apps/web/ui/modals/export-commissions-modal.tsx (1)
  • useExportCommissionsModal (178-198)
apps/web/app/(ee)/api/commissions/export/route.ts (2)
apps/web/lib/zod/schemas/commissions.ts (3)
  • COMMISSION_EXPORT_COLUMNS (213-257)
  • commissionsExportQuerySchema (267-291)
  • getCommissionsQuerySchema (44-98)
apps/web/lib/auth/workspace.ts (1)
  • withWorkspace (41-435)
apps/web/ui/modals/export-commissions-modal.tsx (3)
apps/web/lib/swr/use-workspace.ts (1)
  • useWorkspace (6-45)
apps/web/lib/zod/schemas/commissions.ts (2)
  • DEFAULT_COMMISSION_EXPORT_COLUMNS (262-265)
  • COMMISSION_EXPORT_COLUMNS (213-257)
packages/ui/src/tooltip.tsx (1)
  • InfoTooltip (193-199)
🔇 Additional comments (5)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-popover-buttons.tsx (1)

15-22: Export modal wiring looks good

Popover closes before opening the export modal, modal is mounted at the root of the fragment, and the hook usage is clean.

Also applies to: 42-58

apps/web/lib/zod/schemas/commissions.ts (1)

213-257: Comprehensive, clear column inventory

Good coverage and clear labels/types. This aligns well with the server route’s formatting needs.

apps/web/app/(ee)/api/commissions/export/route.ts (2)

150-166: Confirm header labels vs. field keys in CSV output

If convertToCSV uses object keys for headers, the CSV will show keys like partnerName rather than human-friendly labels like "Partner name". If convertToCSV supports a header map, consider passing labels to match COMMISSION_EXPORT_COLUMNS. Otherwise, prepend a header row using the sorted labels.

Would you like me to adapt convertToCSV usage to include a header mapping derived from columns -> COLUMN_LOOKUP labels?


43-46: Verify route permission gating matches commissions list permissions

The export endpoint should be at least as restricted as viewing commissions. Ensure withWorkspace enforces appropriate permissions/plan. If your pattern is withWorkspace({ requiredPermissions: [...] })(handler) or withWorkspace(handler, { ... }), wire this route accordingly.

I can submit a follow-up patch once you confirm the exact permission string used for reading commissions in this codebase.

apps/web/ui/modals/export-commissions-modal.tsx (1)

178-198: Hook/component pattern reads cleanly

Nice ergonomic API: memoized component + setter. Minimizes prop drilling and keeps the entry point simple.

Comment on lines +90 to +93
a.href = url;
a.download = `Dub Commissions Export - ${new Date().toISOString()}.csv`;
a.click();

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use server-provided filename (or sanitize) and revoke object URL

Avoid illegal colons in filenames on Windows and potential memory leaks by revoking the object URL.

Apply this diff:

-      a.href = url;
-      a.download = `Dub Commissions Export - ${new Date().toISOString()}.csv`;
-      a.click();
+      const disposition = response.headers.get("Content-Disposition");
+      let filename = `Dub Commissions Export - ${new Date()
+        .toISOString()
+        .replace(/:/g, "-")}.csv`;
+      if (disposition) {
+        const match = /filename\*?=(?:UTF-8''|")?([^;\r\n"]+)/i.exec(disposition);
+        if (match?.[1]) {
+          filename = decodeURIComponent(match[1].replace(/^"|"$/g, ""));
+        }
+      }
+
+      a.href = url;
+      a.download = filename;
+      a.click();
+      setTimeout(() => URL.revokeObjectURL(url), 0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
a.href = url;
a.download = `Dub Commissions Export - ${new Date().toISOString()}.csv`;
a.click();
const disposition = response.headers.get("Content-Disposition");
let filename = `Dub Commissions Export - ${new Date()
.toISOString()
.replace(/:/g, "-")}.csv`;
if (disposition) {
const match = /filename\*?=(?:UTF-8''|")?([^;\r\n"]+)/i.exec(disposition);
if (match?.[1]) {
filename = decodeURIComponent(match[1].replace(/^"|"$/g, ""));
}
}
a.href = url;
a.download = filename;
a.click();
setTimeout(() => URL.revokeObjectURL(url), 0);
🤖 Prompt for AI Agents
In apps/web/ui/modals/export-commissions-modal.tsx around lines 90 to 93, the
download filename uses an ISO timestamp with colons (invalid on Windows) and the
created object URL is never revoked causing a potential memory leak; fix by
deriving the filename from the server Content-Disposition header when available
(fall back to a sanitized default), replace or remove illegal characters (e.g.,
replace ":" with "-" in timestamps), set a.download to that sanitized filename,
call a.click(), then revoke the object URL via URL.revokeObjectURL(url) and
clean up the temporary anchor element.

@steven-tey steven-tey merged commit d37937f into main Aug 16, 2025
8 checks passed
@steven-tey steven-tey deleted the export-commissions branch August 16, 2025 17:49
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.

3 participants