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

Skip to content

Conversation

@steven-tey
Copy link
Collaborator

@steven-tey steven-tey commented Nov 25, 2025

Summary by CodeRabbit

  • New Features

    • Partner ban workflow added: ban partners (by partner ID or tenant ID), disable links, cancel pending commissions/payouts, and send ban notifications with audit logging.
  • Documentation

    • OpenAPI updated to include the partner ban operation.
  • Tests

    • End-to-end tests added to validate banning via partner ID and tenant ID.
  • Bug Fixes

    • Consistent validation for partner/tenant identifiers applied across partner analytics and links endpoints.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link
Contributor

vercel bot commented Nov 25, 2025

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

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Nov 26, 2025 5:29pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 25, 2025

Walkthrough

This PR centralizes partnerId/tenantId validation, introduces a new POST /api/partners/ban endpoint and exported banPartner action with expanded ban workflow and side effects, updates partner-related Zod schemas, and adds end-to-end tests covering banning by partnerId and tenantId.

Changes

Cohort / File(s) Summary
Shared validation utility
apps/web/lib/partners/throw-if-no-partnerid-tenantid.ts
Adds throwIfNoPartnerIdOrTenantId(payload) that validates presence of either partnerId or tenantId and throws a DubApiError "bad_request" if both are missing.
Zod schema consolidation
apps/web/lib/zod/schemas/partners.ts
Adds partnerIdTenantIdSchema; updates createPartnerLinkSchema, retrievePartnerLinksSchema, partnerAnalyticsQuerySchema; adds banPartnerApiSchema built from the new schema.
Routes wired to shared validator
apps/web/app/(ee)/api/partners/analytics/route.ts, apps/web/app/(ee)/api/partners/links/route.ts, apps/web/app/(ee)/api/partners/links/upsert/route.ts
Replaces inline partnerId/tenantId presence checks with calls to throwIfNoPartnerIdOrTenantId; control flow otherwise unchanged.
New ban API route
apps/web/app/(ee)/api/partners/ban/route.ts
Adds POST handler validating banPartnerApiSchema, uses throwIfNoPartnerIdOrTenantId, resolves partnerId from tenantId if necessary, enforces plan access, calls banPartner, and returns { partnerId }.
Ban action extraction & expansion
apps/web/lib/actions/partners/ban-partner.ts
Exports new banPartner({ workspace, partnerId, reason, user }) that performs a DB transaction to mark enrollment banned, disable/expire links, cancel commissions/payouts, reject pending bounties, clear discounts, and schedules async side effects (sync totals, cross-program fraud events, cache/Tinybird/discount cleanup, email notification, audit log).
OpenAPI update
apps/web/lib/openapi/partners/ban-partner.ts, apps/web/lib/openapi/partners/index.ts
Adds OpenAPI operation banPartner and registers POST /partners/ban in partners paths.
E2E tests
apps/web/tests/partners/ban-partner.test.ts
Adds integration tests that create a partner and assert banning by partnerId and by tenantId returns 200 and the expected partnerId.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant API as POST /api/partners/ban
    participant Validator as throwIfNoPartnerIdOrTenantId
    participant Resolver as tenantId -> partnerId resolver
    participant Action as banPartner
    participant DB as Database
    participant Async as Async side effects

    Client->>API: POST /partners/ban { partnerId?, tenantId?, reason }
    API->>Validator: Validate partnerId or tenantId present
    alt missing both
        Validator-->>API: throw bad_request
        API-->>Client: 400
    else present
        API->>Resolver: resolve partnerId if tenantId provided
        Resolver-->>API: partnerId
        API->>Action: banPartner(workspace, partnerId, reason, user)
        Action->>DB: begin transaction
        DB->>DB: disable/expire links, update enrollment (banned), cancel commissions/payouts, reject bounties, clear discounts
        DB-->>Action: commit
        Action->>Async: schedule sync totals, fraud events, cache/Tinybird/discount cleanup, send email, audit log
        Action-->>API: { partnerId }
        API-->>Client: 200 { partnerId }
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Areas to pay extra attention:

  • apps/web/lib/actions/partners/ban-partner.ts: transaction correctness, side-effect ordering, error handling, and types for returned enrollment.
  • apps/web/lib/partners/throw-if-no-partnerid-tenantid.ts and all routes now using it: ensure validation behavior matches prior inline checks.
  • apps/web/lib/zod/schemas/partners.ts: schema merges and compatibility with existing clients.
  • apps/web/app/(ee)/api/partners/ban/route.ts: tenantId → partnerId resolution path and access control.
  • apps/web/tests/partners/ban-partner.test.ts: test assumptions vs. actual side-effect timing.

Possibly related PRs

Suggested reviewers

  • devkiran

Poem

🐰 I hopped through schemas, tidy and bright,
Ensured ids present before any fight,
Banned a partner with a gentle thump,
Queued logs and emails, gave caches a bump —
The rabbit celebrates with code and delight.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: adding a new POST endpoint for banning partners in the API.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ban-api

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
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 (4)
apps/web/lib/openapi/partners/ban-partner.ts (1)

6-34: LGTM!

Well-structured OpenAPI operation with clear documentation. The response schema correctly matches the route handler's return type.

Minor note: The description states "mark all commissions as canceled" but the implementation only cancels pending commissions. Consider updating to "mark all pending commissions as canceled" for precision, though this is not blocking.

apps/web/tests/partners/ban-partner.test.ts (2)

14-16: Async initialization in describe callback may cause issues.

The await h.init() executes during test collection (when Vitest parses the describe block), not during test setup. This can lead to race conditions or initialization issues. Consider using beforeAll for async setup:

 describe.sequential("POST /partners/ban", async () => {
   const h = new IntegrationHarness();
-  const { http } = await h.init();
+  let http: Awaited<ReturnType<typeof h.init>>["http"];
+
+  beforeAll(async () => {
+    ({ http } = await h.init());
+  });

   test("ban partner by partnerId", async () => {

44-46: Consider verifying the ban side effects.

The test verifies the API response but doesn't confirm the partner's status actually changed to banned. Consider adding a GET request or database check to verify the ban was applied:

// After ban, verify partner status
const { data: partnerData } = await http.get<Partner>({
  path: `/partners/${createdPartner.id}`,
});
expect(partnerData.status).toBe("banned");
apps/web/lib/zod/schemas/partners.ts (1)

616-629: Description is too specific for a shared schema.

The partnerId description mentions "to create a link for" but this schema is reused for analytics, ban, and retrieve operations. Consider a more generic description:

 export const partnerIdTenantIdSchema = z.object({
   partnerId: z
     .string()
     .nullish()
     .describe(
-      "The ID of the partner to create a link for. Will take precedence over `tenantId` if provided.",
+      "The ID of the partner. Will take precedence over `tenantId` if provided.",
     ),
   tenantId: z
     .string()
     .nullish()
     .describe(
-      "The ID of the partner in your system. If both `partnerId` and `tenantId` are not provided, an error will be thrown.",
+      "The partner's unique ID in your system. If both `partnerId` and `tenantId` are not provided, an error will be thrown.",
     ),
 });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b13e4e and 04aa78f.

📒 Files selected for processing (10)
  • apps/web/app/(ee)/api/partners/analytics/route.ts (2 hunks)
  • apps/web/app/(ee)/api/partners/ban/route.ts (1 hunks)
  • apps/web/app/(ee)/api/partners/links/route.ts (3 hunks)
  • apps/web/app/(ee)/api/partners/links/upsert/route.ts (2 hunks)
  • apps/web/lib/actions/partners/ban-partner.ts (2 hunks)
  • apps/web/lib/openapi/partners/ban-partner.ts (1 hunks)
  • apps/web/lib/openapi/partners/index.ts (2 hunks)
  • apps/web/lib/partners/throw-if-no-partnerid-tenantid.ts (1 hunks)
  • apps/web/lib/zod/schemas/partners.ts (4 hunks)
  • apps/web/tests/partners/ban-partner.test.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (7)
📚 Learning: 2025-06-06T07:59:03.120Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2177
File: apps/web/lib/api/links/bulk-create-links.ts:66-84
Timestamp: 2025-06-06T07:59:03.120Z
Learning: In apps/web/lib/api/links/bulk-create-links.ts, the team accepts the risk of potential undefined results from links.find() operations when building invalidLinks arrays, because existing links are fetched from the database based on the input links, so matches are expected to always exist.

Applied to files:

  • apps/web/app/(ee)/api/partners/links/upsert/route.ts
📚 Learning: 2025-10-17T08:18:19.278Z
Learnt from: devkiran
Repo: dubinc/dub PR: 0
File: :0-0
Timestamp: 2025-10-17T08:18:19.278Z
Learning: In the apps/web codebase, `@/lib/zod` should only be used for places that need OpenAPI extended zod schema. All other places should import from the standard `zod` package directly using `import { z } from "zod"`.

Applied to files:

  • apps/web/lib/openapi/partners/index.ts
📚 Learning: 2025-08-16T11:14:00.667Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2754
File: apps/web/lib/partnerstack/schemas.ts:47-52
Timestamp: 2025-08-16T11:14:00.667Z
Learning: The PartnerStack API always includes the `group` field in partner responses, so the schema should use `.nullable()` rather than `.nullish()` since the field is never omitted/undefined.

Applied to files:

  • apps/web/lib/zod/schemas/partners.ts
📚 Learning: 2025-11-24T09:10:12.494Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3089
File: apps/web/lib/api/fraud/fraud-rules-registry.ts:17-25
Timestamp: 2025-11-24T09:10:12.494Z
Learning: In apps/web/lib/api/fraud/fraud-rules-registry.ts, the fraud rules `partnerCrossProgramBan` and `partnerDuplicatePayoutMethod` intentionally have stub implementations that return `{ triggered: false }` because they are partner-scoped rules handled separately during partner application/onboarding flows (e.g., in detect-record-fraud-application.ts), rather than being evaluated per conversion event like other rules in the registry. The stubs exist only to satisfy the `Record<FraudRuleType, ...>` type constraint.

Applied to files:

  • apps/web/tests/partners/ban-partner.test.ts
  • apps/web/lib/actions/partners/ban-partner.ts
📚 Learning: 2025-11-12T22:23:10.414Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 3098
File: apps/web/lib/actions/partners/message-program.ts:49-58
Timestamp: 2025-11-12T22:23:10.414Z
Learning: In apps/web/lib/actions/partners/message-program.ts, when checking if a partner can continue messaging after messaging is disabled, the code intentionally requires `senderPartnerId: null` (program-initiated messages) to prevent partners from continuing to send junk messages. Only conversations started by the program should continue after messaging is disabled, as a spam prevention mechanism.

Applied to files:

  • apps/web/lib/actions/partners/ban-partner.ts
📚 Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.

Applied to files:

  • apps/web/lib/actions/partners/ban-partner.ts
📚 Learning: 2025-06-18T20:26:25.177Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 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/lib/actions/partners/ban-partner.ts
🧬 Code graph analysis (6)
apps/web/app/(ee)/api/partners/links/upsert/route.ts (1)
apps/web/lib/partners/throw-if-no-partnerid-tenantid.ts (1)
  • throwIfNoPartnerIdOrTenantId (5-14)
apps/web/app/(ee)/api/partners/analytics/route.ts (1)
apps/web/lib/partners/throw-if-no-partnerid-tenantid.ts (1)
  • throwIfNoPartnerIdOrTenantId (5-14)
apps/web/lib/openapi/partners/ban-partner.ts (2)
apps/web/lib/actions/partners/ban-partner.ts (1)
  • banPartner (40-228)
apps/web/lib/zod/schemas/partners.ts (1)
  • banPartnerApiSchema (765-767)
apps/web/lib/openapi/partners/index.ts (2)
apps/web/lib/actions/partners/ban-partner.ts (1)
  • banPartner (40-228)
apps/web/lib/openapi/partners/ban-partner.ts (1)
  • banPartner (6-34)
apps/web/lib/partners/throw-if-no-partnerid-tenantid.ts (2)
apps/web/lib/zod/schemas/partners.ts (1)
  • partnerIdTenantIdSchema (616-629)
apps/web/lib/api/errors.ts (1)
  • DubApiError (58-75)
apps/web/app/(ee)/api/partners/links/route.ts (1)
apps/web/lib/partners/throw-if-no-partnerid-tenantid.ts (1)
  • throwIfNoPartnerIdOrTenantId (5-14)
⏰ 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). (1)
  • GitHub Check: build
🔇 Additional comments (11)
apps/web/app/(ee)/api/partners/links/upsert/route.ts (1)

15-34: LGTM!

Clean refactor to use the centralized throwIfNoPartnerIdOrTenantId helper. The validation is correctly placed after parsing and before any database operations.

apps/web/app/(ee)/api/partners/analytics/route.ts (1)

6-32: LGTM!

Consistent application of the centralized validation helper. Placement after query parsing and before database access is correct.

apps/web/lib/openapi/partners/index.ts (1)

2-27: LGTM!

The new /partners/ban route follows the existing pattern and is correctly wired to the banPartner OpenAPI operation.

apps/web/app/(ee)/api/partners/ban/route.ts (2)

10-38: LGTM on the core logic.

The partnerId resolution flow is correct:

  1. Validation ensures at least one identifier is present
  2. tenantId lookup resolves partnerId when needed
  3. The non-null assertion is safe given the control flow

40-42: Based on my verification, I can now provide the final assessment of the review comment.

The review comment's suggestion to add requiredPermissions is not supported by the codebase pattern.

Out of 12 partner API routes, only 1 (links/upsert) uses requiredPermissions: ["links.write"]. All other partner routes, including the ban route, use only requiredPlan for access control:

  • [partnerId] (GET) — no requiredPermissions
  • analytics — no requiredPermissions
  • ban — no requiredPermissions
  • count — no requiredPermissions
  • export — no requiredPermissions
  • links (GET/POST) — no requiredPermissions

The ban route is consistent with this dominant pattern. The links/upsert operation is an exception that uses requiredPermissions because it directly modifies link resources. Partner-level operations like ban, export, and analytics use only requiredPlan for tier-based access control.

The ban function receives the authenticated user for audit logging and validates the partner exists, which aligns with the architectural approach for other partner operations.

apps/web/lib/partners/throw-if-no-partnerid-tenantid.ts (1)

1-14: LGTM!

Clean utility function that correctly centralizes the partnerId/tenantId validation logic. The falsy check handles null, undefined, and empty strings appropriately. Good application of DRY principle.

apps/web/app/(ee)/api/partners/links/route.ts (1)

9-9: LGTM! Good refactor to centralize validation.

The extraction of throwIfNoPartnerIdOrTenantId into a shared utility improves consistency across partner routes and reduces code duplication. The validation is correctly placed before database operations in both handlers.

Also applies to: 31-32, 89-89

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

765-767: LGTM!

The banPartnerApiSchema correctly composes the shared identifier schema with the required reason field for the ban API endpoint.

apps/web/lib/actions/partners/ban-partner.ts (3)

75-137: Well-structured transaction for atomic ban operations.

The transaction correctly groups all ban-related database updates to ensure consistency. Using updateMany for bulk operations and the composite key for enrollment update is appropriate.


144-154: Good implementation of cross-program fraud detection.

The pattern of creating partnerCrossProgramBan fraud events for other approved program enrollments aligns with the fraud detection architecture. Based on retrieved learnings, pending applications are handled separately during partner application/onboarding flows.

Also applies to: 168-176


194-194: I'll verify the replyTo fallback issue by examining how it's used throughout the codebase and how the email sending layer handles it.
<function_calls>


#!/bin/bash

Search for replyTo patterns in the codebase

echo "=== Searching for replyTo usage ==="
rg -n "replyTo" --type ts -C2 | head -80


</function_calls>

@panda-sandeep
Copy link

/bug0 run

1 similar comment
@panda-sandeep
Copy link

/bug0 run

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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 04aa78f and 867b7ac.

📒 Files selected for processing (2)
  • apps/web/app/(ee)/api/partners/ban/route.ts (1 hunks)
  • apps/web/lib/actions/partners/ban-partner.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/app/(ee)/api/partners/ban/route.ts
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-11-12T22:23:10.414Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 3098
File: apps/web/lib/actions/partners/message-program.ts:49-58
Timestamp: 2025-11-12T22:23:10.414Z
Learning: In apps/web/lib/actions/partners/message-program.ts, when checking if a partner can continue messaging after messaging is disabled, the code intentionally requires `senderPartnerId: null` (program-initiated messages) to prevent partners from continuing to send junk messages. Only conversations started by the program should continue after messaging is disabled, as a spam prevention mechanism.

Applied to files:

  • apps/web/lib/actions/partners/ban-partner.ts
📚 Learning: 2025-11-24T09:10:12.536Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3089
File: apps/web/lib/api/fraud/fraud-rules-registry.ts:17-25
Timestamp: 2025-11-24T09:10:12.536Z
Learning: In apps/web/lib/api/fraud/fraud-rules-registry.ts, the fraud rules `partnerCrossProgramBan` and `partnerDuplicatePayoutMethod` intentionally have stub implementations that return `{ triggered: false }` because they are partner-scoped rules handled separately during partner application/onboarding flows (e.g., in detect-record-fraud-application.ts), rather than being evaluated per conversion event like other rules in the registry. The stubs exist only to satisfy the `Record<FraudRuleType, ...>` type constraint.

Applied to files:

  • apps/web/lib/actions/partners/ban-partner.ts
📚 Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.

Applied to files:

  • apps/web/lib/actions/partners/ban-partner.ts
📚 Learning: 2025-06-18T20:26:25.177Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 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/lib/actions/partners/ban-partner.ts
🧬 Code graph analysis (1)
apps/web/lib/actions/partners/ban-partner.ts (10)
apps/web/lib/openapi/partners/ban-partner.ts (1)
  • banPartner (6-34)
packages/prisma/client.ts (2)
  • PartnerBannedReason (23-23)
  • ProgramEnrollmentStatus (31-31)
apps/web/lib/types.ts (1)
  • UserProps (244-256)
apps/web/lib/api/partners/sync-total-commissions.ts (1)
  • syncTotalCommissions (5-50)
apps/web/lib/api/fraud/resolve-fraud-events.ts (1)
  • resolveFraudEvents (6-73)
apps/web/lib/tinybird/record-link.ts (1)
  • recordLink (75-86)
apps/web/lib/api/discounts/queue-discount-code-deletion.ts (1)
  • queueDiscountCodeDeletion (13-37)
packages/email/src/index.ts (1)
  • sendEmail (6-29)
apps/web/lib/zod/schemas/partners.ts (1)
  • BAN_PARTNER_REASONS (66-73)
apps/web/lib/api/audit-logs/record-audit-log.ts (1)
  • recordAuditLog (47-73)
⏰ 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). (1)
  • GitHub Check: build
🔇 Additional comments (7)
apps/web/lib/actions/partners/ban-partner.ts (7)

27-39: Delegation pattern looks good.

The refactoring to extract banPartner as a separate function improves reusability and testability while keeping the action wrapper clean.


41-65: Function signature and setup are well-structured.

The use of Pick types for workspace and user parameters ensures type safety while keeping dependencies minimal. The eager loading of related entities (links, discountCodes, program, partner) is appropriate for the subsequent operations.


67-72: Status validation correctly implemented.

The use of DubApiError with code: "bad_request" properly addresses the past review concern about error handling, ensuring the API returns an appropriate 4xx response instead of a 500 error.


143-181: Side effects orchestration is well-structured.

The use of waitUntil for post-transaction side effects is appropriate, allowing the response to be sent while cleanup and notifications continue. The parallel operations with Promise.allSettled handle failures gracefully.

The cross-program fraud detection logic (lines 148-180) correctly:

  • Fetches other approved enrollments for the same partner
  • Creates partnerCrossProgramBan fraud events to flag potential risks
  • Resolves existing fraud events for the banned program

183-192: Pre-transaction enrollment data is acceptable here.

Using the original programEnrollment (fetched before the transaction) for cleanup operations is appropriate. The links and discount codes that existed at ban time are exactly what need to be expired from caches, deleted from Tinybird, and queued for deletion.


213-226: Audit logging correctly captures the ban action.

The audit log properly records the workspace, program, action type, actor, and target partner with metadata, providing a complete audit trail.


79-141: Review comment is incorrect—the current API response contract is documented and intentional.

The OpenAPI definition at apps/web/lib/openapi/partners/ban-partner.ts explicitly documents the response schema as:

{
  "partnerId": "string"
}

The current code correctly returns { partnerId }, matching this documented contract. Changing it to return updatedEnrollment would break the API contract for existing clients.

If API consumers need the full enrollment details, they should:

  • Query the enrollment separately after the ban
  • Or propose updating the OpenAPI schema and response together as a breaking change

The transaction operations themselves are correct and properly structured; returning only partnerId is the intended behavior.

Comment on lines +194 to 211
partner.email &&
sendEmail({
subject: `You've been banned from the ${program.name} Partner Program`,
to: partner.email,
replyTo: program.supportEmail || "noreply",
react: PartnerBanned({
partner: {
name: partner.name,
email: partner.email,
},
],
program: {
name: program.name,
slug: program.slug,
},
bannedReason: BAN_PARTNER_REASONS[reason],
}),
variant: "notifications",
}),
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 | 🟡 Minor

Invalid email address in replyTo fallback.

The replyTo field falls back to the string "noreply" (line 198), which is not a valid email address. This could cause email delivery failures or rejections by email providers.

Apply this fix to use a valid email address:

-            replyTo: program.supportEmail || "noreply",
+            replyTo: program.supportEmail || "[email protected]",
🤖 Prompt for AI Agents
In apps/web/lib/actions/partners/ban-partner.ts around lines 194–211 the replyTo
fallback uses the invalid string "noreply"; replace it with a valid email
address (e.g. program.supportEmail ||
`noreply@${process.env.DEFAULT_EMAIL_DOMAIN || 'example.com'}`) or another
project-standard no-reply mailbox, ensure the value conforms to an email format,
and add/Document the DEFAULT_EMAIL_DOMAIN env var if needed so the fallback is
always a valid email address.

@steven-tey steven-tey merged commit 9fbcb94 into main Nov 26, 2025
7 of 8 checks passed
@steven-tey steven-tey deleted the ban-api branch November 26, 2025 19:35
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.

4 participants