-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Add POST /partners/ban API
#3156
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThis 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Areas to pay extra attention:
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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.
Example instruction:
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. Comment |
There was a problem hiding this 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 indescribecallback 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 usingbeforeAllfor 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
partnerIddescription 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
📒 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.tsapps/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
throwIfNoPartnerIdOrTenantIdhelper. 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/banroute follows the existing pattern and is correctly wired to thebanPartnerOpenAPI operation.apps/web/app/(ee)/api/partners/ban/route.ts (2)
10-38: LGTM on the core logic.The partnerId resolution flow is correct:
- Validation ensures at least one identifier is present
- tenantId lookup resolves partnerId when needed
- 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
requiredPermissionsis 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 onlyrequiredPlanfor access control:
[partnerId](GET) — no requiredPermissionsanalytics— no requiredPermissionsban— no requiredPermissionscount— no requiredPermissionsexport— no requiredPermissionslinks(GET/POST) — no requiredPermissionsThe ban route is consistent with this dominant pattern. The
links/upsertoperation is an exception that usesrequiredPermissionsbecause it directly modifies link resources. Partner-level operations like ban, export, and analytics use onlyrequiredPlanfor 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
throwIfNoPartnerIdOrTenantIdinto 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
banPartnerApiSchemacorrectly composes the shared identifier schema with the requiredreasonfield 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
updateManyfor bulk operations and the composite key for enrollment update is appropriate.
144-154: Good implementation of cross-program fraud detection.The pattern of creating
partnerCrossProgramBanfraud 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 thereplyTofallback issue by examining how it's used throughout the codebase and how the email sending layer handles it.
<function_calls>
#!/bin/bashSearch for replyTo patterns in the codebase
echo "=== Searching for replyTo usage ==="
rg -n "replyTo" --type ts -C2 | head -80
</function_calls>
|
/bug0 run |
1 similar comment
|
/bug0 run |
There was a problem hiding this 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
📒 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
banPartneras 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
Picktypes 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
DubApiErrorwithcode: "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
waitUntilfor post-transaction side effects is appropriate, allowing the response to be sent while cleanup and notifications continue. The parallel operations withPromise.allSettledhandle failures gracefully.The cross-program fraud detection logic (lines 148-180) correctly:
- Fetches other approved enrollments for the same partner
- Creates
partnerCrossProgramBanfraud 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.tsexplicitly documents the response schema as:{ "partnerId": "string" }The current code correctly returns
{ partnerId }, matching this documented contract. Changing it to returnupdatedEnrollmentwould 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
partnerIdis the intended behavior.
| 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", | ||
| }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Summary by CodeRabbit
New Features
Documentation
Tests
Bug Fixes
✏️ Tip: You can customize this high-level summary in your review settings.