Skip to content

Applications API endpoints - #3787

Merged
steven-tey merged 15 commits into
mainfrom
approve-reject-endpoints
Apr 24, 2026
Merged

Applications API endpoints#3787
steven-tey merged 15 commits into
mainfrom
approve-reject-endpoints

Conversation

@devkiran

@devkiran devkiran commented Apr 20, 2026

Copy link
Copy Markdown
Collaborator
  • Implemented POST /api/partners/approve to approve pending partner applications.
  • Implemented POST /api/partners/reject to reject pending partner applications.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added APIs to approve and reject pending partner applications with customizable rejection reasons, detailed notes, and optional immediate reapply functionality
  • Documentation

    • Updated API documentation descriptions across multiple endpoints to clarify pagination behavior and endpoint functionality for improved developer experience

- Implemented POST /api/partners/approve to approve pending partner applications.
- Implemented POST /api/partners/reject to reject pending partner applications.
- Updated partner approval and rejection logic to include workspace context and user permissions.
- Enhanced OpenAPI documentation for both endpoints.
- Refactored partner approval and rejection actions to streamline database interactions and email notifications.
@vercel

vercel Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
dub Ready Ready Preview Apr 24, 2026 9:52pm

Request Review

@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

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

This PR refactors partner application approval and rejection workflows by extracting inline server action logic into dedicated API layer functions (approvePartner, rejectPartner). New API routes and OpenAPI definitions are introduced. Schema validation is updated to require workspaceId at the action boundary while core schemas are cleaned up with documentation. An E2E test helper endpoint and integration tests verify the full workflow.

Changes

Cohort / File(s) Summary
Core API Functions
apps/web/lib/api/partners/applications/approve-partner.ts, apps/web/lib/api/partners/applications/reject-partner.ts
New approvePartner and rejectPartner functions encapsulating enrollment validation, database transactions, and async post-approval workflows (activity logging, workflow triggers, rejection emails).
Server Actions
apps/web/lib/actions/partners/approve-partner.ts, apps/web/lib/actions/partners/reject-partner-application.ts
Refactored to delegate core logic to API layer functions while maintaining input validation and permission checks; input schemas extended with required workspaceId.
API Routes
apps/web/app/(ee)/api/partners/applications/approve/route.ts, apps/web/app/(ee)/api/partners/applications/reject/route.ts
New POST endpoints for partner application approval/rejection with workspace authorization and plan/role gating.
OpenAPI Definitions
apps/web/lib/openapi/partners/approve-partner.ts, apps/web/lib/openapi/partners/reject-partner.ts, apps/web/lib/openapi/partners/index.ts
Added operation definitions and route mappings for new approve/reject endpoints with request/response schemas and token security.
Zod Schemas
apps/web/lib/zod/schemas/partners.ts
Removed workspaceId from approvePartnerSchema and rejectPartnerSchema; added descriptive metadata to fields.
E2E Testing
apps/web/app/(ee)/api/e2e/partners/pending-program-application/route.ts, apps/web/tests/partners/applications/approve-reject-partner-application.test.ts
New helper endpoint for managing partner application state transitions in tests; integration test suite covering full approve/reject workflow.
Supporting Updates
apps/web/app/(ee)/api/cron/partners/auto-approve/route.ts, apps/web/lib/api-logs/constants.ts
Auto-approval cron switched to new approvePartner function; route patterns extended for new endpoints.
OpenAPI Metadata
apps/web/lib/openapi/commissions/list-commissions.ts, apps/web/lib/openapi/customers/get-customer.ts, apps/web/lib/openapi/customers/get-customers.ts, apps/web/lib/openapi/domains/list-domains.ts, apps/web/lib/openapi/events/index.ts, apps/web/lib/openapi/folders/list-folders.ts, apps/web/lib/openapi/links/get-links.ts, apps/web/lib/openapi/partners/list-partner-applications.ts, apps/web/lib/openapi/payouts/list-payouts.ts, apps/web/lib/openapi/tags/get-tags.ts
Updated OpenAPI operation descriptions and summaries to clarify pagination behavior and endpoint purpose across multiple endpoints.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant Route as API Route
    participant Action as Server Action
    participant API as API Function
    participant DB as Database
    participant Async as Async Tasks

    Client->>Route: POST /partners/applications/approve
    Route->>Route: Validate input (approvePartnerSchema)
    Route->>Route: Derive workspace & user context
    Route->>Action: Call approvePartnerAction()
    Action->>API: Call approvePartner()
    API->>DB: Fetch programEnrollment
    API->>DB: Validate enrollment status = pending
    API->>DB: Start transaction
    API->>DB: Check trial enrollment limits
    API->>DB: Update enrollment to approved
    API->>DB: Update programApplication metadata
    API->>DB: Commit transaction
    API->>Async: Schedule activity log & partner-approved workflow
    API-->>Action: Return partnerId
    Action-->>Route: Return approval result
    Route-->>Client: 200 JSON {partnerId}
    Async->>Async: Record activity & trigger workflow
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Add activity logs for partner status changes #3746: Modifies the same server action files with logging changes, directly related to the refactored approval/rejection workflow.
  • Free Trial #3644: Touches the same partner approval/rejection flow and the approvePartner/rejectPartner API layer functions being introduced.
  • Request logs #3719: Updates the same ROUTE_PATTERNS constant in apps/web/lib/api-logs/constants.ts for route logging.

Suggested reviewers

  • pepeladeira

🐰 Approvals and rejections now flow through the API layer,
Server actions delegate, like a careful librarian.
Transactions lock the database tight,
While workflows dance in the async night.

🚥 Pre-merge checks | ✅ 3 | ❌ 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 'Applications API endpoints' is overly vague and does not clearly convey the primary change. While it relates to the changeset, it fails to specify that the endpoints are for approving and rejecting partner applications. Consider revising to be more specific, such as 'Add partner application approval and rejection API endpoints' or 'Implement approve and reject endpoints for partner applications'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch approve-reject-endpoints

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.

@devkiran
devkiran marked this pull request as ready for review April 21, 2026 11:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

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/partners/approve-partner-enrollment.ts (1)

40-60: ⚠️ Potential issue | 🟠 Major

Guard approval to pending enrollments only.

Line 41 updates by (partnerId, programId) without checking status, so the new approval endpoint can turn rejected, deactivated, or banned enrollments back to approved while logging the old status as "pending". Make the transition conditional on status: "pending" and fail when no pending row is updated.

🛡️ Proposed fix
-    const enrollment = await tx.programEnrollment.update({
-      where: {
-        partnerId_programId: {
-          partnerId,
-          programId,
-        },
-      },
+    const { count } = await tx.programEnrollment.updateMany({
+      where: {
+        partnerId,
+        programId,
+        status: "pending",
+      },
       data: {
         status: "approved",
         createdAt: new Date(),
         groupId: group.id,
         clickRewardId: group.clickRewardId,
         leadRewardId: group.leadRewardId,
         saleRewardId: group.saleRewardId,
         discountId: group.discountId,
       },
+    });
+
+    if (count === 0) {
+      throw new Error("Partner enrollment must be pending to approve.");
+    }
+
+    const enrollment = await tx.programEnrollment.findUniqueOrThrow({
+      where: {
+        partnerId_programId: {
+          partnerId,
+          programId,
+        },
+      },
       include: {
         partner: true,
       },
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/partners/approve-partner-enrollment.ts` around lines 40 - 60,
The update currently uses tx.programEnrollment.update(...) with the unique key
and no status guard, allowing non-pending enrollments to be re-approved; change
this to a conditional update that only affects rows with status: "pending" by
using tx.programEnrollment.updateMany({ where: { partnerId, programId, status:
"pending" }, data: { ... } }) inside the transaction, check the returned count
and throw an error (or return a failure) if count === 0, and then fetch the
updated enrollment (e.g., tx.programEnrollment.findUnique or findFirst) to get
the partner include; replace references to tx.programEnrollment.update with this
two-step updateMany + fetch flow so the transition is guarded to pending-only.
🤖 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/api/partners/reject-partner.ts`:
- Around line 71-80: The deletion of the pending enrollment
(tx.programEnrollment.deleteMany for programEnrollment.id with status "pending")
is not verified, so a concurrent change could leave the row undeleted; update
the block guarded by allowImmediateReapply to capture the result of
tx.programEnrollment.deleteMany, check result.count (or count field) and if it
is 0 throw an error or handle it as a failed deletion (e.g., throw new
Error("Failed to delete pending enrollment")) so the caller knows the deletion
did not occur; keep this check inside the same transaction and only return after
confirming count > 0.

In `@apps/web/lib/openapi/partners/approve-partner.ts`:
- Around line 12-18: The OpenAPI operation in approve-partner.ts currently
defines requestBody with content but omits requestBody.required; update the
requestBody object for this operation to include required: true so the
requestBody (schema referenced by approvePartnerSchema) is explicitly mandatory
for clients and generated SDKs. Locate the requestBody definition in the
approve-partner.ts OpenAPI object and add required: true alongside the existing
content entry referencing approvePartnerSchema.

In `@apps/web/lib/openapi/partners/reject-partner.ts`:
- Around line 12-18: The OpenAPI operation in reject-partner.ts currently omits
requestBody.required (so the body is treated as optional); update the
operation's requestBody object to include required: true so that the generated
SDK enforces the body and partnerId presence—i.e., add required: true beside the
existing content block that references rejectPartnerSchema in the requestBody
for this endpoint.

In `@apps/web/lib/zod/schemas/partners.ts`:
- Around line 862-871: The schema description for approvePartnerSchema (field
groupId) is inconsistent with the approvePartnerEnrollment implementation which
uses groupId || program.defaultGroupId; either update approvePartnerEnrollment
to implement the intended precedence (e.g., use current enrollment.groupId >
passed groupId > program.defaultGroupId) or change the approvePartnerSchema
description to unambiguously match current behavior, and if you can’t change
behavior now, add a tracked follow-up (issue/PR) and simplify the schema comment
to state the actual runtime precedence (passed groupId or
program.defaultGroupId) so the public API contract is not misleading.

---

Outside diff comments:
In `@apps/web/lib/partners/approve-partner-enrollment.ts`:
- Around line 40-60: The update currently uses tx.programEnrollment.update(...)
with the unique key and no status guard, allowing non-pending enrollments to be
re-approved; change this to a conditional update that only affects rows with
status: "pending" by using tx.programEnrollment.updateMany({ where: { partnerId,
programId, status: "pending" }, data: { ... } }) inside the transaction, check
the returned count and throw an error (or return a failure) if count === 0, and
then fetch the updated enrollment (e.g., tx.programEnrollment.findUnique or
findFirst) to get the partner include; replace references to
tx.programEnrollment.update with this two-step updateMany + fetch flow so the
transition is guarded to pending-only.
🪄 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: f6bd368a-c585-4af1-8ae8-3685dc742844

📥 Commits

Reviewing files that changed from the base of the PR and between 6f1e40a and 9c604ae.

📒 Files selected for processing (10)
  • apps/web/app/(ee)/api/partners/approve/route.ts
  • apps/web/app/(ee)/api/partners/reject/route.ts
  • apps/web/lib/actions/partners/approve-partner.ts
  • apps/web/lib/actions/partners/reject-partner-application.ts
  • apps/web/lib/api/partners/reject-partner.ts
  • apps/web/lib/openapi/partners/approve-partner.ts
  • apps/web/lib/openapi/partners/index.ts
  • apps/web/lib/openapi/partners/reject-partner.ts
  • apps/web/lib/partners/approve-partner-enrollment.ts
  • apps/web/lib/zod/schemas/partners.ts

Comment thread apps/web/lib/api/partners/applications/reject-partner.ts
Comment thread apps/web/lib/openapi/partners/approve-partner.ts
Comment thread apps/web/lib/openapi/partners/reject-partner.ts
Comment thread apps/web/lib/zod/schemas/partners.ts
- Improved error handling in the approvePartner function to provide clearer feedback on enrollment status.
- Updated the logic to determine the final group ID for partner assignments.
- Enhanced comments and descriptions in the API and OpenAPI documentation for clarity.
- Removed unnecessary code and streamlined database interactions.
- Updated error messages in the rejectPartner function for clarity on enrollment status.
- Added validation to ensure that only pending enrollments can be deleted, with appropriate error feedback.
- Marked requestBody as required in the OpenAPI documentation for both reject and approve partner endpoints.
@devkiran

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review please

@coderabbitai

coderabbitai Bot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

@devkiran Sure! I'll kick off a full review of PR #3787 right away.

✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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/api/partners/reject-partner.ts`:
- Around line 117-167: The Promise.allSettled call inside waitUntil discards
rejected results so failed post-rejection side effects (trackActivityLog,
resolveFraudGroups, sendEmail) are never surfaced; update the waitUntil block to
inspect the Promise.allSettled results and log any failures (include the
operation identity and error) so failures in trackActivityLog,
resolveFraudGroups, or sendEmail are visible for investigation — e.g., run
Promise.allSettled([...]) and iterate the settled results, calling
processLogger.error or your logger with the task name
(trackActivityLog/resolveFraudGroups/sendEmail), the rejection reason, and
contextual IDs (workspaceId, programId, partnerId, userId).
- Around line 100-112: The non-immediate-reapply branch currently uses
tx.programEnrollment.update which can throw if the enrollment status was changed
concurrently; replace that call with tx.programEnrollment.updateMany filtering
on id and status: "pending", then validate the returned count is 1 (otherwise
throw or return the same controlled error path), mirroring the
immediate-reapply/deleteMany pattern; update references:
tx.programEnrollment.update -> tx.programEnrollment.updateMany and ensure you
set status to ProgramEnrollmentStatus.rejected and null out
clickRewardId/leadRewardId/saleRewardId/discountId, then check the count to
handle concurrent status changes consistently.
🪄 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: 1c17b7da-9e33-4d73-9b50-c401848d0e0b

📥 Commits

Reviewing files that changed from the base of the PR and between 6ab1499 and bed1f5e.

📒 Files selected for processing (2)
  • apps/web/lib/api-logs/constants.ts
  • apps/web/lib/api/partners/reject-partner.ts

Comment thread apps/web/lib/api/partners/applications/reject-partner.ts
Comment thread apps/web/lib/api/partners/applications/reject-partner.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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/partners/approve-partner.ts (1)

77-125: ⚠️ Potential issue | 🟠 Major

Activity log records stale status — variable shadowing makes new always equal "pending".

Inside the transaction callback, const programEnrollment (Line 78) shadows the outer programEnrollment loaded at Line 21. Once the transaction returns, the reference at Line 123 (new: programEnrollment.status) resolves to the outer object, whose status is the pre-update value ("pending", as enforced by the check at Lines 51–57). The activity log will always emit status: { old: "pending", new: "pending" }, silently breaking audit fidelity for partner approvals.

Either hoist the updated enrollment out of the transaction, or hard-code the new status (since it's always "approved" on success). Renaming the inner binding also avoids the shadowing.

🛠️ Proposed fix
-  await prisma.$transaction(async (tx) => {
-    const programEnrollment = await tx.programEnrollment.update({
+  const updatedEnrollment = await prisma.$transaction(async (tx) => {
+    const updated = await tx.programEnrollment.update({
       where: {
         partnerId_programId: {
           partnerId,
           programId,
         },
       },
       data: {
         status: "approved",
         createdAt: new Date(),
         groupId: group.id,
         clickRewardId: group.clickRewardId,
         leadRewardId: group.leadRewardId,
         saleRewardId: group.saleRewardId,
         discountId: group.discountId,
       },
     });

-    if (programEnrollment.applicationId) {
+    if (updated.applicationId) {
       await tx.programApplication.update({
         where: {
-          id: programEnrollment.applicationId,
+          id: updated.applicationId,
         },
         data: {
           reviewedAt: new Date(),
           rejectionReason: null,
           rejectionNote: null,
           userId,
         },
       });
     }
+
+    return updated;
   });

   waitUntil(
     Promise.allSettled([
       trackActivityLog({
         workspaceId: program.workspace.id,
         programId,
         resourceType: "partner",
         resourceId: partnerId,
         userId,
         action: "partner.approved",
         changeSet: {
           status: {
             old: "pending",
-            new: programEnrollment.status,
+            new: updatedEnrollment.status,
           },
         },
       }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/partners/approve-partner.ts` around lines 77 - 125, The activity
log uses the outer programEnrollment (pre-update) because the inner const
programEnrollment inside the prisma.$transaction shadows it; fix by returning
the updated enrollment from the transaction and assigning it to a new variable
(e.g., updatedEnrollment) or by renaming the inner binding (e.g.,
txProgramEnrollment) and then use updatedEnrollment.status (or hard-code
"approved") when calling trackActivityLog so the changeSet new value reflects
the post-update status; update the prisma.$transaction callback (and the
Promise.allSettled call site using trackActivityLog) to reference the
non-shadowed variable (updatedEnrollment or the hard-coded "approved") instead
of the outer programEnrollment.
♻️ Duplicate comments (2)
apps/web/lib/api/partners/reject-partner.ts (2)

100-112: ⚠️ Potential issue | 🟠 Major

Use updateMany with count validation for the non-immediate rejection path.

This branch still uses update with a pending-status guard. If the enrollment changes after Line 56 but before this transaction runs, Prisma can throw instead of returning the controlled DubApiError used by the delete branch. Mirror the Line 81 count-check pattern here.

Proposed fix
-    await tx.programEnrollment.update({
+    const { count } = await tx.programEnrollment.updateMany({
       where: {
         id: programEnrollment.id,
         status: "pending",
       },
       data: {
         status: ProgramEnrollmentStatus.rejected,
         clickRewardId: null,
         leadRewardId: null,
         saleRewardId: null,
         discountId: null,
       },
     });
+
+    if (count !== 1) {
+      throw new DubApiError({
+        code: "bad_request",
+        message:
+          "This enrollment cannot be rejected because it is no longer pending.",
+      });
+    }

Optional verification: confirm whether global API error handling already maps Prisma P2025 into a client-safe 400; if not, this fix prevents a race from surfacing as a 500.

#!/bin/bash
# Description: Inspect whether Prisma not-found/update misses are globally translated
# into controlled client errors. Expect: explicit handling of P2025 if keeping update().

rg -n -C3 'P2025|PrismaClientKnownRequestError|isRecordNotFound|Record to update not found' --type ts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/api/partners/reject-partner.ts` around lines 100 - 112, The
current non-immediate rejection uses tx.programEnrollment.update with a
pending-status guard (tx.programEnrollment.update) which can throw if the record
changed before the transaction; change this to tx.programEnrollment.updateMany
and then check the returned count (as done for the delete branch) to ensure
exactly one row was updated, and throw the same controlled DubApiError when
count !== 1; keep the data payload (status: ProgramEnrollmentStatus.rejected,
clickRewardId: null, leadRewardId: null, saleRewardId: null, discountId: null)
and the same where filter (id: programEnrollment.id, status: "pending") but
perform a count validation after updateMany to mirror the Line 81 pattern.

117-167: ⚠️ Potential issue | 🟡 Minor

Log rejected side effects instead of discarding allSettled failures.

allSettled prevents side-effect failures from blocking the response, which is good, but the rejected results are currently dropped. That makes activity-log, fraud-resolution, or rejection-email failures invisible.

Proposed fix
   waitUntil(
     Promise.allSettled([
       trackActivityLog({
         workspaceId: workspace.id,
         programId,
         resourceType: "partner",
         resourceId: partnerId,
         userId,
         action: "partner_application.rejected",
         changeSet: {
           status: {
             old: "pending",
             new: "rejected",
           },
         },
       }),
 
       resolveFraudGroups({
         where: {
           programId,
           partnerId,
         },
         userId,
         resolutionReason:
           "Resolved automatically because the partner application was rejected.",
       }),
 
       partner.email &&
         sendEmail({
           to: partner.email,
           subject: `Your application to ${program.name} was not approved`,
           variant: "notifications",
           replyTo: program.supportEmail || "noreply",
           react: PartnerApplicationRejected({
             partner: {
               name: partner.name ?? "there",
               email: partner.email,
             },
             program: {
               name: program.name,
               slug: program.slug,
               supportEmail: program.supportEmail,
             },
             additionalNotes: rejectionNote,
             rejectionReason:
               getProgramApplicationRejectionReasonLabel(rejectionReason),
             canReapplyImmediately: allowImmediateReapply,
           }),
         }),
-    ]),
+    ]).then((results) => {
+      results.forEach((result, index) => {
+        if (result.status === "rejected") {
+          console.error("Failed to run partner rejection side effect", {
+            task: ["trackActivityLog", "resolveFraudGroups", "sendEmail"][index],
+            workspaceId: workspace.id,
+            programId,
+            partnerId,
+            userId,
+            error: result.reason,
+          });
+        }
+      });
+    }),
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/api/partners/reject-partner.ts` around lines 117 - 167, The
Promise.allSettled call inside waitUntil swallows failures; change it to capture
the settled results (e.g., const results = await Promise.allSettled([...])
inside the same async context used by waitUntil) and then iterate over results
to find entries with status === "rejected" and log their reason (use the
project's logger or processLogger) along with context (workspaceId, programId,
partnerId) so failures from trackActivityLog, resolveFraudGroups, or sendEmail
(PartnerApplicationRejected) are recorded instead of discarded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@apps/web/lib/partners/approve-partner.ts`:
- Around line 77-125: The activity log uses the outer programEnrollment
(pre-update) because the inner const programEnrollment inside the
prisma.$transaction shadows it; fix by returning the updated enrollment from the
transaction and assigning it to a new variable (e.g., updatedEnrollment) or by
renaming the inner binding (e.g., txProgramEnrollment) and then use
updatedEnrollment.status (or hard-code "approved") when calling trackActivityLog
so the changeSet new value reflects the post-update status; update the
prisma.$transaction callback (and the Promise.allSettled call site using
trackActivityLog) to reference the non-shadowed variable (updatedEnrollment or
the hard-coded "approved") instead of the outer programEnrollment.

---

Duplicate comments:
In `@apps/web/lib/api/partners/reject-partner.ts`:
- Around line 100-112: The current non-immediate rejection uses
tx.programEnrollment.update with a pending-status guard
(tx.programEnrollment.update) which can throw if the record changed before the
transaction; change this to tx.programEnrollment.updateMany and then check the
returned count (as done for the delete branch) to ensure exactly one row was
updated, and throw the same controlled DubApiError when count !== 1; keep the
data payload (status: ProgramEnrollmentStatus.rejected, clickRewardId: null,
leadRewardId: null, saleRewardId: null, discountId: null) and the same where
filter (id: programEnrollment.id, status: "pending") but perform a count
validation after updateMany to mirror the Line 81 pattern.
- Around line 117-167: The Promise.allSettled call inside waitUntil swallows
failures; change it to capture the settled results (e.g., const results = await
Promise.allSettled([...]) inside the same async context used by waitUntil) and
then iterate over results to find entries with status === "rejected" and log
their reason (use the project's logger or processLogger) along with context
(workspaceId, programId, partnerId) so failures from trackActivityLog,
resolveFraudGroups, or sendEmail (PartnerApplicationRejected) are recorded
instead of discarded.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c424996c-1be7-4e33-95b5-7cea4108241c

📥 Commits

Reviewing files that changed from the base of the PR and between 6f1e40a and bed1f5e.

📒 Files selected for processing (12)
  • apps/web/app/(ee)/api/cron/partners/auto-approve/route.ts
  • apps/web/app/(ee)/api/partners/approve/route.ts
  • apps/web/app/(ee)/api/partners/reject/route.ts
  • apps/web/lib/actions/partners/approve-partner.ts
  • apps/web/lib/actions/partners/reject-partner-application.ts
  • apps/web/lib/api-logs/constants.ts
  • apps/web/lib/api/partners/reject-partner.ts
  • apps/web/lib/openapi/partners/approve-partner.ts
  • apps/web/lib/openapi/partners/index.ts
  • apps/web/lib/openapi/partners/reject-partner.ts
  • apps/web/lib/partners/approve-partner.ts
  • apps/web/lib/zod/schemas/partners.ts

@devkiran devkiran changed the title Add API endpoints for approving and rejecting partner applications Add POST /api/partners/[approve, reject] Apr 22, 2026
@devkiran devkiran changed the title Add POST /api/partners/[approve, reject] Add POST /api/partners/approve and POST /api/partners/reject Apr 22, 2026
@steven-tey steven-tey changed the title Add POST /api/partners/approve and POST /api/partners/reject Applications API endpoints Apr 23, 2026
@steven-tey

Copy link
Copy Markdown
Collaborator

@coderabbitai full review, any breaking changes?

@coderabbitai

coderabbitai Bot commented Apr 24, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Checking for remaining references to approvePartnerEnrollment ==="
rg -n "approvePartnerEnrollment" --type ts

echo ""
echo "=== Checking for callers passing workspaceId to approvePartnerSchema or rejectPartnerSchema ==="
rg -n "approvePartnerSchema\|rejectPartnerSchema" --type ts -A 3

echo ""
echo "=== Check for all usages of approvePartner and rejectPartner ==="
rg -n "approvePartner\|rejectPartner" --type ts -B 1 -A 1

echo ""
echo "=== Check approve-partner.ts full content ==="
fd "approve-partner.ts" --exec cat {}

echo ""
echo "=== Check reject-partner.ts full content ==="
fd "reject-partner.ts" --exec cat {}

echo ""
echo "=== Check partners.ts schema file ==="
fd "partners.ts" apps/web/lib/zod/schemas --exec cat {}

echo ""
echo "=== Check approve route ==="
fd "route.ts" apps/web/app/\(ee\)/api/partners/approve --exec cat {}

echo ""
echo "=== Check reject route ==="
fd "route.ts" apps/web/app/\(ee\)/api/partners/reject --exec cat {}

echo ""
echo "=== Check auto-approve cron route ==="
fd "route.ts" apps/web/app/\(ee\)/api/cron/partners/auto-approve --exec cat {}

echo ""
echo "=== Check approve-partner action ==="
fd "approve-partner.ts" apps/web/lib/actions --exec cat {}

echo ""
echo "=== Check reject-partner-application action ==="
fd "reject-partner-application.ts" apps/web/lib/actions --exec cat {}

echo ""
echo "=== Check for any tests related to approve/reject partner ==="
rg -n "approvePartner\|rejectPartner\|approve-partner\|reject-partner" --type ts -g "*test*" -g "*spec*"

Length of output: 47223


@steven-tey — here's the full review with a specific focus on breaking changes:


✅ Breaking Changes

There are no external breaking changes. Here's the breakdown:

Change Breaking? Notes
approvePartnerEnrollmentapprovePartner ✅ No All internal callsites updated (cron route, server actions, new API routes). Zero remaining references to the old name.
workspaceId removed from approvePartnerSchema / rejectPartnerSchema ✅ No workspaceId was never part of the public API contract. Server actions re-extend locally with .extend({ workspaceId: z.string() }).
New POST /api/partners/approve & POST /api/partners/reject endpoints ✅ No Purely additive.
rejectPartner logic extracted from inline server action ✅ No Server action now delegates to rejectPartner(); behavior is preserved.

🔍 Issues Found

1. Semantic bug: activity log records wrong status when allowImmediateReapply=true

In apps/web/lib/api/partners/reject-partner.ts, when allowImmediateReapply is true, the enrollment row is deleted — but waitUntil still fires with:

changeSet: {
  status: {
    old: "pending",
    new: "rejected",  // ❌ enrollment was deleted, not moved to "rejected"
  },
},

The new status should reflect deletion (e.g. "deleted" or "removed") or the activity log event should differ from the standard rejection path.


2. Inconsistent activity log action naming

approvePartner uses "partner.approved" (dot separator), while rejectPartner uses "partner_application.rejected" (underscore separator). Recommend aligning to one convention.


3. OpenAPI tag inconsistency — "Partner Applications" vs "Partners"

Both new OpenAPI definitions use tags: ["Partner Applications"]. Please verify this is intentional and consistent with other partner management endpoints (e.g. ban, deactivate, archive). If those use "Partners", the SDK grouping will be split.


4. No tests

No test files exist for the new endpoints or the extracted rejectPartner / approvePartner API functions. Given the transaction logic complexity (especially the allowImmediateReapply branch), test coverage would be valuable.


5. Minor: approvePartner waitUntil activity log changeSet

changeSet: {
  status: {
    old: "pending",
    new: programEnrollment.status,  // ⚠️ This is still "pending" since it's the pre-update snapshot
  },
},

programEnrollment.status here references the pre-transaction value fetched at the top of the function (which is "pending"). The new value should be hardcoded to "approved" for clarity.


🏁 Script executed:

#!/bin/bash
echo "=== Check OpenAPI tags used across partner endpoint definitions ==="
rg -n '"tags"' apps/web/lib/openapi/partners/ -A 1

echo ""
echo "=== Approve route content ==="
fd "route.ts" --search-path apps/web/app -E "node_modules" | xargs grep -l "approve" | head -5 | xargs cat

echo ""
echo "=== Reject route content ==="
fd "route.ts" --search-path apps/web/app -E "node_modules" | xargs grep -l "reject" | head -5 | xargs cat

Length of output: 42449


@steven-tey — here's the complete review with a breaking-changes focus:


✅ Breaking Changes

No external or internal breaking changes. Full verification:

Change Breaking? Notes
approvePartnerEnrollmentapprovePartner rename ✅ No Zero remaining references to the old name — cron route, server actions, and new API routes all updated consistently.
workspaceId removed from approvePartnerSchema / rejectPartnerSchema ✅ No workspaceId was never exposed in the public API contract. Server actions re-add it locally via .extend({ workspaceId: z.string() }).
New POST /api/partners/approve & POST /api/partners/reject ✅ No Purely additive endpoints.
rejectPartner extracted from inline server action ✅ No Server action now delegates; behavior is preserved.

🔍 Issues Found

1. 🐛 Bug: allowImmediateReapply=true logs wrong status

In apps/web/lib/api/partners/reject-partner.ts, when allowImmediateReapply is true, the enrollment row is deleted — but waitUntil still fires with:

changeSet: {
  status: {
    old: "pending",
    new: "rejected",  // ❌ enrollment was deleted, not transitioned to "rejected"
  },
},

The activity log action and changeSet should reflect the deletion, or a separate conditional branch should be used for the allowImmediateReapply path.


2. 🐛 Bug: approvePartner activity log captures stale "pending" status

In apps/web/lib/partners/approve-partner.ts, the waitUntil block uses:

changeSet: {
  status: {
    old: "pending",
    new: programEnrollment.status,  // ❌ this is the pre-transaction snapshot = "pending"
  },
},

programEnrollment.status was fetched before the transaction that updates it to "approved". The new value should be hardcoded to "approved".


3. ⚠️ High-impact inconsistency: auto-reject cron bypasses rejectPartner()

apps/web/app/(ee)/api/cron/partners/auto-reject/route.ts has its own inline rejection logic (Prisma transaction, resolveFraudGroups, email sending) and does not call the new rejectPartner() function. This means rejection behavior can diverge between:

  • POST /api/partners/reject + server action → uses rejectPartner()
  • Auto-reject cron → uses its own inline path

For example, if rejectPartner() is updated in the future, the cron will silently fall out of sync. The cron should be refactored to call rejectPartner() (or a shared internal helper) for consistency.


4. Activity log action naming inconsistency

approvePartner uses action: "partner.approved" (dot separator), while rejectPartner uses action: "partner_application.rejected" (underscore separator). These should follow one convention.


5. No tests

No test files exist for approvePartner, rejectPartner, or the new API routes. Given the transaction branching complexity in rejectPartner (especially the allowImmediateReapply path), unit/integration tests are strongly recommended.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
apps/web/tests/partners/applications/approve-reject-partner-application.test.ts (1)

8-91: Good E2E coverage — consider asserting activity log entries too.

The sequential flow exercises reject → duplicate reject (400) → reset → approve → GET correctly. One gap: the test only inspects partner.status, so it wouldn't catch the stale changeSet.status.new bug flagged in approve-partner.ts (line 131) or the changeSet mismatch in the immediate-reapply rejection path. A lightweight DB assertion against activityLog for the approve/reject records would make this suite a stronger regression guard.

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

In
`@apps/web/tests/partners/applications/approve-reject-partner-application.test.ts`
around lines 8 - 91, Add assertions that query the activityLog after the reject
and approve requests to ensure corresponding entries exist and contain correct
payloads; specifically after the reject POST check activityLog for a rejection
entry tied to partnerId with rejectionReason/rejectionNote and correct changeSet
(e.g., changeSet.status.old/new reflect the transition), and after the approve
POST check activityLog for an approval entry tied to partnerId with
changeSet.status.new === "approved" and any groupId present; locate these checks
around the uses of the endpoints "/partners/applications/reject" and
"/partners/applications/approve" in the test "reject then approve the same
partner after E2E pending reset" and fail the test if expected activityLog
records are missing or have mismatched changeSet/status values.
apps/web/lib/api/partners/applications/approve-partner.ts (1)

127-127: Consider aligning activity log action naming with the reject flow.

This uses "partner.approved" while reject-partner.ts (line 125) uses "partner_application.rejected". Pick one convention (e.g., both under a partner_application.* namespace) so downstream activity consumers don't have to special-case each side of the same workflow.

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

In `@apps/web/lib/api/partners/applications/approve-partner.ts` at line 127, The
activity log action in apps/web/lib/api/partners/applications/approve-partner.ts
currently uses "partner.approved" which is inconsistent with the reject flow
that uses "partner_application.rejected"; update the action string to the same
namespace (e.g., change "partner.approved" to "partner_application.approved")
where the action property is set so both approve and reject flows share the
"partner_application.*" convention and downstream consumers don't need
special-casing.
🤖 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/api/partners/applications/approve-partner.ts`:
- Around line 119-134: The activity log is using the outer-scoped
programEnrollment (the pre-update findUnique) so changeSet.status.new is stuck
as "pending"; inside the transaction callback rename the updated record (e.g.,
to updatedEnrollment) and ensure you capture that updatedEnrollment in a
variable accessible when calling waitUntil/trackActivityLog (or hoist a
reference to the updated status after the transaction) so trackActivityLog uses
the post-transaction status; update references to programEnrollment in the
trackActivityLog call to use the new updatedEnrollment (or its status) instead
of the outer programEnrollment to avoid shadowing.

In `@apps/web/lib/api/partners/applications/reject-partner.ts`:
- Around line 117-132: The activity log always records changeSet.status.new =
"rejected" in the trackActivityLog call even when allowImmediateReapply === true
and the enrollment row is deleted; update the logic in the reject partner flow
to branch the changeSet based on allowImmediateReapply (use
allowImmediateReapply flag in the code path that calls trackActivityLog) so that
when allowImmediateReapply is true you record a different outcome (e.g.,
changeSet.status.new = "deleted" or set a distinct action like
"partner_application.deleted") rather than "rejected"; locate the
trackActivityLog invocation in reject-partner.ts and alter the payload
construction to conditionally set changeSet/status or action using
allowImmediateReapply so logs accurately reflect deletion vs rejection.

---

Nitpick comments:
In `@apps/web/lib/api/partners/applications/approve-partner.ts`:
- Line 127: The activity log action in
apps/web/lib/api/partners/applications/approve-partner.ts currently uses
"partner.approved" which is inconsistent with the reject flow that uses
"partner_application.rejected"; update the action string to the same namespace
(e.g., change "partner.approved" to "partner_application.approved") where the
action property is set so both approve and reject flows share the
"partner_application.*" convention and downstream consumers don't need
special-casing.

In
`@apps/web/tests/partners/applications/approve-reject-partner-application.test.ts`:
- Around line 8-91: Add assertions that query the activityLog after the reject
and approve requests to ensure corresponding entries exist and contain correct
payloads; specifically after the reject POST check activityLog for a rejection
entry tied to partnerId with rejectionReason/rejectionNote and correct changeSet
(e.g., changeSet.status.old/new reflect the transition), and after the approve
POST check activityLog for an approval entry tied to partnerId with
changeSet.status.new === "approved" and any groupId present; locate these checks
around the uses of the endpoints "/partners/applications/reject" and
"/partners/applications/approve" in the test "reject then approve the same
partner after E2E pending reset" and fail the test if expected activityLog
records are missing or have mismatched changeSet/status values.
🪄 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: 551eeb61-5f48-444e-83e4-a0b4a17cf9fb

📥 Commits

Reviewing files that changed from the base of the PR and between bed1f5e and 71d515f.

📒 Files selected for processing (23)
  • apps/web/app/(ee)/api/cron/partners/auto-approve/route.ts
  • apps/web/app/(ee)/api/e2e/partners/pending-program-application/route.ts
  • apps/web/app/(ee)/api/partners/applications/approve/route.ts
  • apps/web/app/(ee)/api/partners/applications/reject/route.ts
  • apps/web/lib/actions/partners/approve-partner.ts
  • apps/web/lib/actions/partners/reject-partner-application.ts
  • apps/web/lib/api-logs/constants.ts
  • apps/web/lib/api/partners/applications/approve-partner.ts
  • apps/web/lib/api/partners/applications/reject-partner.ts
  • apps/web/lib/openapi/commissions/list-commissions.ts
  • apps/web/lib/openapi/customers/get-customer.ts
  • apps/web/lib/openapi/customers/get-customers.ts
  • apps/web/lib/openapi/domains/list-domains.ts
  • apps/web/lib/openapi/events/index.ts
  • apps/web/lib/openapi/folders/list-folders.ts
  • apps/web/lib/openapi/links/get-links.ts
  • apps/web/lib/openapi/partners/approve-partner.ts
  • apps/web/lib/openapi/partners/index.ts
  • apps/web/lib/openapi/partners/list-partner-applications.ts
  • apps/web/lib/openapi/partners/reject-partner.ts
  • apps/web/lib/openapi/payouts/list-payouts.ts
  • apps/web/lib/openapi/tags/get-tags.ts
  • apps/web/tests/partners/applications/approve-reject-partner-application.test.ts
✅ Files skipped from review due to trivial changes (11)
  • apps/web/lib/openapi/payouts/list-payouts.ts
  • apps/web/lib/openapi/events/index.ts
  • apps/web/lib/openapi/tags/get-tags.ts
  • apps/web/lib/openapi/domains/list-domains.ts
  • apps/web/lib/openapi/links/get-links.ts
  • apps/web/lib/openapi/folders/list-folders.ts
  • apps/web/lib/openapi/customers/get-customers.ts
  • apps/web/lib/api-logs/constants.ts
  • apps/web/lib/openapi/partners/list-partner-applications.ts
  • apps/web/lib/openapi/customers/get-customer.ts
  • apps/web/lib/openapi/commissions/list-commissions.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/web/lib/openapi/partners/index.ts
  • apps/web/app/(ee)/api/cron/partners/auto-approve/route.ts
  • apps/web/lib/openapi/partners/approve-partner.ts
  • apps/web/lib/actions/partners/reject-partner-application.ts
  • apps/web/lib/actions/partners/approve-partner.ts

Comment thread apps/web/lib/api/partners/applications/approve-partner.ts
Comment thread apps/web/lib/api/partners/applications/reject-partner.ts
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