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

Skip to content

Conversation

@steven-tey
Copy link
Collaborator

@steven-tey steven-tey commented Sep 1, 2025

Summary by CodeRabbit

  • New Features

    • Export program applications to CSV from the Applications menu with selectable columns and instant download (Business/Enterprise plans).
  • Style

    • Reworked Applications menu to a responsive grid layout and added an Export Applications action + modal.
    • Refreshed Export Partners modal layout, header, and column/filter controls; added Cancel action.
  • Bug Fixes

    • More robust commission amount and currency handling to avoid recording zero/invalid amounts.
  • Chores

    • Minor CSV download header formatting tweak (no behavior change).

@vercel
Copy link
Contributor

vercel bot commented Sep 1, 2025

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

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Sep 1, 2025 8:14pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 1, 2025

Walkthrough

Adds a new CSV export flow for program applications (server route, modal, and menu integration), introduces export column schema constants, tweaks partners export route header string, and updates export-related modals and commission/schema handling elsewhere.

Changes

Cohort / File(s) Summary
Program applications export route
apps/web/app/(ee)/api/programs/[programId]/applications/export/route.ts
New GET route: workspace-auth + plan check, accepts optional columns query, fetches pending program enrollments/applications, maps/validates rows via dynamic Zod schema, returns CSV (text/csv, Content-Disposition attachment).
Partners export route
apps/web/app/(ee)/api/partners/export/route.ts
Minor header change: Content-Disposition value switched from a template literal to a plain string "attachment" (no behavioral change).
Applications menu UI
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/applications/applications-menu.tsx
Replaced cmdk/menu items with a custom grid-based popover UI; added "Export Applications" action and "View rejected partners"; integrates useExportApplicationsModal and renders modal.
Export modal (applications)
apps/web/ui/modals/export-applications-modal.tsx
New modal component + useExportApplicationsModal hook: select export columns, trigger GET export, download CSV blob, show toasts, handle errors and loading.
Export modal (partners)
apps/web/ui/modals/export-partners-modal.tsx
UI rework: header/body layout changes, removed logo, columns grid wrapped in container, filters simplified to useFilters switch, added Cancel button; submit/export flow preserved.
Export schemas
apps/web/lib/zod/schemas/partners.ts
Added exportApplicationColumns (id/label metadata) and exportApplicationsColumnsDefault (default column IDs) for applications export.
Tolt: commission import & schemas
apps/web/lib/tolt/import-commissions.ts, apps/web/lib/tolt/schemas.ts
import-commissions: replaced sale.amount with sale.revenue ?? 0saleAmount, conditional recording/updates when saleAmount > 0, updated flows to use saleAmount. schemas.ts: made revenue and charge_id nullable and updated descriptions.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Menu as Applications Menu (UI)
  participant Modal as ExportApplicationsModal (UI)
  participant API as Server: /api/programs/:programId/applications/export
  participant DB as Prisma
  participant CSV as CSV generator
  participant Browser as Browser Download

  User->>Menu: open menu
  Menu->>Modal: open export modal
  User->>Modal: select columns, click Export
  Modal->>API: GET /api/programs/:id/applications/export?columns=...
  Note over API: auth + plan check
  API->>DB: query pending enrollments + includes
  DB-->>API: enrollment + application data
  API->>CSV: map rows, validate (dynamic Zod), convert to CSV
  CSV-->>API: CSV text
  API-->>Modal: 200 OK (text/csv)
  Modal->>Browser: create blob & trigger download
  Browser-->>User: save "applications-<timestamp>.csv"
  Modal->>Modal: close modal
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • devkiran

Poem

A rabbit taps keys in a bright little burrow,
"Columns selected — CSV, ready to go!"
Rows hop together in tidy array,
A modal, a click, then a file on its way.
Data delivered — a carrot-shaped commando. 🐇📥

✨ 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-applications

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 or @coderabbit 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.

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

🧹 Nitpick comments (17)
apps/web/app/(ee)/api/partners/export/route.ts (1)

79-84: Include filename and charset in CSV response headers

Improve download UX and cross-browser compatibility by adding a filename and explicit charset.

     return new Response(convertToCSV(formattedPartners), {
       headers: {
-        "Content-Type": "text/csv",
-        "Content-Disposition": "attachment",
+        "Content-Type": "text/csv; charset=utf-8",
+        "Content-Disposition": `attachment; filename="partners.csv"; filename*=UTF-8''partners.csv`,
       },
     });
apps/web/lib/zod/schemas/partners.ts (2)

76-91: Co-locate “default” flags with column metadata and tighten typing

This keeps defaults DRY and enables safer reuse by routes and UI.

-export const exportApplicationColumns = [
-  { id: "id", label: "ID" },
-  { id: "name", label: "Name" },
-  { id: "email", label: "Email" },
-  { id: "country", label: "Country" },
-  { id: "createdAt", label: "Applied at" },
-  { id: "description", label: "Description" },
-  { id: "website", label: "Website" },
-  { id: "youtube", label: "YouTube" },
-  { id: "twitter", label: "Twitter" },
-  { id: "linkedin", label: "LinkedIn" },
-  { id: "instagram", label: "Instagram" },
-  { id: "tiktok", label: "TikTok" },
-  { id: "proposal", label: "Proposal" },
-  { id: "comments", label: "Comments" },
-];
+export const exportApplicationColumns = [
+  { id: "id", label: "ID", default: true },
+  { id: "name", label: "Name", default: true },
+  { id: "email", label: "Email", default: true },
+  { id: "country", label: "Country", default: true },
+  { id: "createdAt", label: "Applied at", default: true },
+  { id: "description", label: "Description" },
+  { id: "website", label: "Website", default: true },
+  { id: "youtube", label: "YouTube", default: true },
+  { id: "twitter", label: "Twitter" },
+  { id: "linkedin", label: "LinkedIn", default: true },
+  { id: "instagram", label: "Instagram" },
+  { id: "tiktok", label: "TikTok" },
+  { id: "proposal", label: "Proposal" },
+  { id: "comments", label: "Comments" },
+] as const;

Optionally also export:

export type ApplicationExportColumnId =
  (typeof exportApplicationColumns)[number]["id"];

93-102: Derive defaults from the source list

Avoids drift between the columns list and defaults.

-export const exportApplicationsColumnsDefault = [
-  "id",
-  "name",
-  "email",
-  "country",
-  "createdAt",
-  "website",
-  "youtube",
-  "linkedin",
-];
+export const exportApplicationsColumnsDefault = exportApplicationColumns
+  .filter((c) => c.default)
+  .map((c) => c.id);
apps/web/ui/modals/export-partners-modal.tsx (4)

112-151: Columns section UX: prevent exporting with zero columns

If users unselect all, the API falls back to defaults, which can surprise. Either disable Export when none selected or show a helper/error.


74-79: Prefer Accept header over Content-Type on GET

Advertise expected response type.

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

86-93: Revoke object URL after download

Avoids small memory leaks in long-lived sessions.

       const a = document.createElement("a");

       a.href = url;
       a.download = `Dub Partners Export - ${new Date().toISOString()}.csv`;
       a.click();
+      a.remove();
+      window.URL.revokeObjectURL(url);

96-99: Surface error messages cleanly in toast

Ensure string is shown, not an Error object.

-    } catch (error) {
-      toast.error(error);
+    } catch (error) {
+      const message =
+        error instanceof Error ? error.message : "Failed to export partners.";
+      toast.error(message);
apps/web/app/(ee)/api/programs/[programId]/applications/export/route.ts (2)

84-89: Add filename and charset to response headers

Aligns with partners export and improves UX.

-    return new Response(convertToCSV(formattedApplications), {
+    return new Response(convertToCSV(formattedApplications), {
       headers: {
-        "Content-Type": "text/csv",
-        "Content-Disposition": "attachment",
+        "Content-Type": "text/csv; charset=utf-8",
+        "Content-Disposition": `attachment; filename="applications.csv"; filename*=UTF-8''applications.csv`,
       },
     });

29-38: Consider paging/upper bound for very large exports

Unbounded findMany on pending can be heavy. Either cap (e.g., 10k), stream, or batch.

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/applications/applications-menu.tsx (3)

85-99: Use a “disable” icon for disabling auto-approve (UX clarity).

Swap the red UserCheck for UserXmark to better match the action semantics.

-                    <UserCheck className="size-4 shrink-0 text-red-600" />
+                    <UserXmark className="size-4 shrink-0 text-red-600" />

115-123: Guard against missing workspaceSlug before navigation.

Prevents pushing an /undefined/... route if slug hasn’t resolved yet.

-                  router.push(
-                    `/${workspaceSlug}/program/partners/applications/rejected`,
-                  );
+                  if (!workspaceSlug) {
+                    toast.error("Workspace not loaded yet.");
+                    return;
+                  }
+                  router.push(`/${workspaceSlug}/program/partners/applications/rejected`);

156-161: Disable menu trigger until workspace context is ready.

Avoids confirm actions that rely on workspaceId! when it’s not yet available.

-          disabled={isUpdatingAutoApprove || !program}
+          disabled={
+            isUpdatingAutoApprove || !program || !workspaceId || !workspaceSlug
+          }
apps/web/ui/modals/export-applications-modal.tsx (5)

7-7: Remove unused router helper.

useRouterStuff/getQueryString is imported but unused; will trip lint.

-import { Button, Checkbox, Modal, useRouterStuff } from "@dub/ui";
+import { Button, Checkbox, Modal } from "@dub/ui";
@@
-  const { getQueryString } = useRouterStuff();

Also applies to: 33-34


45-49: Surface missing context to the user.

Early return is silent; show a toast so users know what happened.

-    if (!workspaceId || !program?.id) {
-      return;
-    }
+    if (!workspaceId || !program?.id) {
+      toast.error("Workspace or program not loaded. Please try again.");
+      return;
+    }

53-66: Avoid setting Content-Type on GET; add small hardening.

Unnecessary header can cause preflight; drop it.

-      const response = await fetch(
+      const response = await fetch(
         `/api/programs/${program.id}/applications/export?${new URLSearchParams({
           workspaceId: workspaceId,
           ...(data.columns.length
             ? { columns: data.columns.join(",") }
             : undefined),
         })}`,
-        {
-          method: "GET",
-          headers: {
-            "Content-Type": "application/json",
-          },
-        },
+        { method: "GET" },
       );

113-126: Checkbox tri-state and dedup guard.

Ensure boolean handling and prevent accidental duplicates.

-                          onCheckedChange={(checked) => {
-                            field.onChange(
-                              checked
-                                ? [...field.value, id]
-                                : field.value.filter((value) => value !== id),
-                            );
-                          }}
+                          onCheckedChange={(checked) => {
+                            field.onChange(
+                              checked === true
+                                ? Array.from(new Set([...field.value, id]))
+                                : field.value.filter((value) => value !== id),
+                            );
+                          }}

151-155: Optionally disable Export when no columns selected.

Prevents empty submissions and clarifies state.

-          <Button
+          <Button
             type="submit"
             loading={isSubmitting}
             text="Export applications"
             className="h-8 w-fit px-3"
+            disabled={isSubmitting /* plus: watch length if desired */}
           />

If you want full UX, I can wire up useWatch to disable when columns.length === 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 b2075f5 and 5d4c900.

📒 Files selected for processing (6)
  • apps/web/app/(ee)/api/partners/export/route.ts (1 hunks)
  • apps/web/app/(ee)/api/programs/[programId]/applications/export/route.ts (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/applications/applications-menu.tsx (2 hunks)
  • apps/web/lib/zod/schemas/partners.ts (1 hunks)
  • apps/web/ui/modals/export-applications-modal.tsx (1 hunks)
  • apps/web/ui/modals/export-partners-modal.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
apps/web/ui/modals/export-applications-modal.tsx (2)
apps/web/lib/swr/use-workspace.ts (1)
  • useWorkspace (6-45)
apps/web/lib/zod/schemas/partners.ts (2)
  • exportApplicationsColumnsDefault (93-102)
  • exportApplicationColumns (76-91)
apps/web/app/(ee)/api/programs/[programId]/applications/export/route.ts (3)
apps/web/lib/zod/schemas/partners.ts (2)
  • exportApplicationColumns (76-91)
  • exportApplicationsColumnsDefault (93-102)
apps/web/app/(ee)/api/partners/export/route.ts (1)
  • GET (21-96)
apps/web/lib/auth/workspace.ts (1)
  • withWorkspace (41-435)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/applications/applications-menu.tsx (1)
apps/web/ui/modals/export-applications-modal.tsx (1)
  • useExportApplicationsModal (162-182)
apps/web/ui/modals/export-partners-modal.tsx (2)
apps/web/lib/zod/schemas/partners.ts (1)
  • exportPartnerColumns (29-61)
packages/ui/src/tooltip.tsx (1)
  • InfoTooltip (193-199)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Vade Review
  • GitHub Check: build
🔇 Additional comments (7)
apps/web/ui/modals/export-partners-modal.tsx (3)

108-110: Header polish LGTM

Clear, concise header structure.


153-167: “Apply current filters” control LGTM

Good clarity and tooltip usage.


169-183: Actions area LGTM

Cancel button is a helpful addition; submit state is wired.

apps/web/app/(ee)/api/programs/[programId]/applications/export/route.ts (1)

68-79: Investigate convertToCSV for built-in cell sanitization

Please verify whether convertToCSV already escapes leading =, +, -, or @. If it does not, prepend a single quote within route.ts as shown.

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/applications/applications-menu.tsx (2)

67-75: Nice modal integration.

Hook + once-mounted modal pattern looks clean and avoids remount glitches.


135-151: Export entrypoint looks good.

Clear label and closes popover before opening modal. Solid UX.

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

90-159: Overall modal structure looks solid.

Form wiring, defaults, and success path are clear.

Comment on lines +69 to +70
const { error } = await response.json();
throw new Error(error.message);
Copy link
Contributor

Choose a reason for hiding this comment

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

Error handling assumes error response has a specific structure but doesn't handle cases where the response isn't valid JSON or lacks the expected error.message property.

View Details
📝 Patch Details
diff --git a/apps/web/ui/modals/export-applications-modal.tsx b/apps/web/ui/modals/export-applications-modal.tsx
index 423c08354..cd034466c 100644
--- a/apps/web/ui/modals/export-applications-modal.tsx
+++ b/apps/web/ui/modals/export-applications-modal.tsx
@@ -66,8 +66,14 @@ function ExportApplicationsModal({
       );
 
       if (!response.ok) {
-        const { error } = await response.json();
-        throw new Error(error.message);
+        let errorMessage = "Export failed";
+        try {
+          const errorData = await response.json();
+          errorMessage = errorData?.error?.message || errorData?.message || errorMessage;
+        } catch {
+          // If JSON parsing fails, use default message
+        }
+        throw new Error(errorMessage);
       }
 
       const blob = await response.blob();

Analysis

The error handling on lines 69-70 assumes that when response.ok is false, the response body will be valid JSON with an error object containing a message property. However, this code doesn't handle several potential failure scenarios:

  1. If the response body is not valid JSON, response.json() will throw an error
  2. If the JSON doesn't have an error property, the destructuring will fail
  3. If error.message is undefined, new Error(undefined) creates an error with message "undefined"

This can lead to confusing error messages or unhandled promise rejections. The error handling should be more robust:

if (!response.ok) {
  let errorMessage = 'Export failed';
  try {
    const errorData = await response.json();
    errorMessage = errorData?.error?.message || errorData?.message || errorMessage;
  } catch {
    // If JSON parsing fails, use default message
  }
  throw new Error(errorMessage);
}

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/programs/[programId]/applications/export/route.ts (2)

16-22: Sanitize and allow‑list “columns” to avoid undefined CSV headers

Current split can yield empty/unknown ids → “undefined” header cells. Trim, dedupe, and filter against known ids; require at least one valid column.

@@
-const applicationsExportQuerySchema = z.object({
-  columns: z
-    .string()
-    .optional()
-    .transform((v) => v?.split(",") || exportApplicationsColumnsDefault)
-    .default(exportApplicationsColumnsDefault.join(",")),
-});
+const allowedApplicationColumnIds = new Set(
+  exportApplicationColumns.map((c) => c.id),
+);
+
+const applicationsExportQuerySchema = z.object({
+  columns: z
+    .string()
+    .optional()
+    .default(exportApplicationsColumnsDefault.join(","))
+    .transform((v) =>
+      v.split(",").map((s) => s.trim()).filter(Boolean),
+    )
+    .transform((arr) =>
+      Array.from(new Set(arr.filter((id) => allowedApplicationColumnIds.has(id)))),
+    )
+    .refine((arr) => arr.length > 0, { message: "No valid columns specified" }),
+});

25-29: Honor [programId] path param and enforce workspace scoping

Route ignores the path param and always uses the default program. Validate the param belongs to the current workspace and query by it to avoid exporting the wrong program’s data.

-export const GET = withWorkspace(
-  async ({ searchParams, workspace }) => {
-    const programId = getDefaultProgramIdOrThrow(workspace);
+export const GET = withWorkspace(
+  async ({ searchParams, params, workspace }) => {
+    const { programId } = params as { programId: string };
+    const program = await prisma.program.findFirst({
+      where: { id: programId, workspaceId: workspace.id },
+      select: { id: true },
+    });
+    if (!program) {
+      return new Response("Program not found", { status: 404 });
+    }
@@
-    const programEnrollments = await prisma.programEnrollment.findMany({
+    const programEnrollments = await prisma.programEnrollment.findMany({
       where: {
-        programId,
+        programId: program.id,
         status: "pending",
       },
       include: {
         partner: true,
         application: true,
       },
+      orderBy: { createdAt: "desc" },
+      take: 5000,
     });

Additional (outside this hunk): remove the now-unused import if present.

-import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw";

Also applies to: 31-35

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

65-71: Clarify revenue unit and confirm null handling

  • Update description in apps/web/lib/tolt/schemas.ts:
-    .describe("Revenue of the transaction in cents."),
+    .describe("Revenue of the transaction in the smallest currency unit (e.g., cents for USD)."),
  • Downstream usage: sale.revenue is null-coalesced to 0 in apps/web/lib/tolt/import-commissions.ts:214; no other numeric conversions of .revenue and no external uses of charge_id.
apps/web/lib/tolt/import-commissions.ts (3)

213-215: Harden parsing of nullable revenue strings

Guard against NaN/non-numeric values to avoid propagating NaN into writes and comparisons.

-  // Sale amount (can potentially be null)
-  let saleAmount = Number(sale.revenue ?? 0);
+  // Sale amount (nullable string from Tolt) -> integer smallest unit
+  const rawRevenue = sale.revenue;
+  let saleAmount =
+    typeof rawRevenue === "string" ? Number.parseInt(rawRevenue, 10) : 0;
+  if (!Number.isFinite(saleAmount)) saleAmount = 0;

253-254: Avoid false-positive dedupe when saleAmount is 0

Zero-amount imports within the ±1h window may collide. Consider applying the amount match only when saleAmount > 0.

-      amount: saleAmount,
+      ...(saleAmount > 0 ? { amount: saleAmount } : {}),

324-324: Confirm business rule: create 0-amount “sale” commissions?

You still insert a commission when saleAmount is 0. If these shouldn’t be persisted, gate the create by saleAmount > 0; otherwise, all good.

apps/web/app/(ee)/api/programs/[programId]/applications/export/route.ts (2)

65-69: Avoid rebuilding Zod schema per row; add typing

Hoist the row schema to reduce per-item allocations and add a concrete type for schemaFields.

-    const schemaFields = {};
+    const schemaFields: Record<string, z.ZodTypeAny> = {};
     columns.forEach((column) => {
       schemaFields[columnIdToLabel[column]] = z.string().optional().default("");
     });
 
-    const formattedApplications = applications.map((application) => {
+    const rowSchema = z.object(schemaFields);
+
+    const formattedApplications = applications.map((application) => {
@@
-      return z.object(schemaFields).parse(result);
+      return rowSchema.parse(result);
     });

Also applies to: 83-84


86-91: Set a download filename in Content‑Disposition

Minor UX win and aligns with common CSV routes.

   return new Response(convertToCSV(formattedApplications), {
     headers: {
       "Content-Type": "text/csv",
-      "Content-Disposition": "attachment",
+      "Content-Disposition": 'attachment; filename="applications.csv"',
     },
   });
📜 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 5d4c900 and fd9c30b.

📒 Files selected for processing (3)
  • apps/web/app/(ee)/api/programs/[programId]/applications/export/route.ts (1 hunks)
  • apps/web/lib/tolt/import-commissions.ts (5 hunks)
  • apps/web/lib/tolt/schemas.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-18T20:26:25.177Z
Learnt from: TWilson023
PR: dubinc/dub#2538
File: apps/web/ui/partners/overview/blocks/commissions-block.tsx:16-27
Timestamp: 2025-06-18T20:26:25.177Z
Learning: In the Dub codebase, components that use workspace data (workspaceId, defaultProgramId) are wrapped in `WorkspaceAuth` which ensures these values are always available, making non-null assertions safe. This is acknowledged as a common pattern in their codebase, though not ideal.

Applied to files:

  • apps/web/app/(ee)/api/programs/[programId]/applications/export/route.ts
🧬 Code graph analysis (2)
apps/web/app/(ee)/api/programs/[programId]/applications/export/route.ts (3)
apps/web/lib/zod/schemas/partners.ts (2)
  • exportApplicationColumns (76-91)
  • exportApplicationsColumnsDefault (93-102)
apps/web/app/(ee)/api/partners/export/route.ts (1)
  • GET (21-96)
apps/web/lib/auth/workspace.ts (1)
  • withWorkspace (41-435)
apps/web/lib/tolt/import-commissions.ts (1)
apps/web/lib/analytics/convert-currency.ts (1)
  • convertCurrencyWithFxRates (57-92)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Vade Review
  • GitHub Check: build
🔇 Additional comments (4)
apps/web/lib/tolt/import-commissions.ts (4)

219-224: LGTM: reuse converted saleAmount

Using convertCurrencyWithFxRates and reassigning saleAmount is correct and consistent with earnings conversion.


335-347: LGTM: gate sale event emission on positive amount

This prevents noisy “Invoice paid” events for zero/negative amounts.


349-349: LGTM: conditional link stats updates

Incrementing sales/saleAmount only when saleAmount > 0 is appropriate. If refunds are expected to arrive here with negative amounts, confirm they’re handled elsewhere (e.g., separate refund flow) so aggregates reconcile.

Also applies to: 363-370


374-389: LGTM: conditional customer stats updates

Same note as link stats regarding refunds; otherwise this aligns with the gating logic.

Comment on lines +42 to +51
const applications = programEnrollments.map(
({ partner, application, ...programEnrollment }) => {
return {
...partner,
createdAt: application?.createdAt || programEnrollment.createdAt,
proposal: application?.proposal || "",
comments: application?.comments || "",
};
},
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Populate “description” from application; currently always empty in CSV

You map proposal/comments but omit description, so selected “Description” column exports blanks.

     return {
       ...partner,
       createdAt: application?.createdAt || programEnrollment.createdAt,
+      description: application?.description || "",
       proposal: application?.proposal || "",
       comments: application?.comments || "",
     };
📝 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
const applications = programEnrollments.map(
({ partner, application, ...programEnrollment }) => {
return {
...partner,
createdAt: application?.createdAt || programEnrollment.createdAt,
proposal: application?.proposal || "",
comments: application?.comments || "",
};
},
);
const applications = programEnrollments.map(
({ partner, application, ...programEnrollment }) => {
return {
...partner,
createdAt: application?.createdAt || programEnrollment.createdAt,
description: application?.description || "",
proposal: application?.proposal || "",
comments: application?.comments || "",
};
},
);
🤖 Prompt for AI Agents
In apps/web/app/(ee)/api/programs/[programId]/applications/export/route.ts
around lines 42 to 51, the mapped application objects include proposal and
comments but omit description so the exported CSV "Description" column is blank;
update the returned object to include description: set it to
application?.description || "" (similar to proposal/comments) so description is
populated from the application when present and falls back to an empty string.

@steven-tey steven-tey merged commit a3c8c2c into main Sep 1, 2025
9 checks passed
@steven-tey steven-tey deleted the export-applications branch September 1, 2025 20:32
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