-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Add bulk ban UI #3158
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
Add bulk ban UI #3158
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdds a cron POST handler and helper to process post-ban work asynchronously, a batch QStash enqueue utility, UI and server changes to support bulk banning (modal, table wiring, server actions), and refactors single/bulk ban actions to enqueue external cron jobs while simplifying in-process side effects. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant UI as Dashboard UI
participant User
participant Action as bulkBanPartnersAction
participant Enqueue as enqueueBatchJobs / QStash
participant Cron as /api/cron/partners/ban/process
participant DB as Database
User->>UI: Select partners & confirm ban
UI->>Action: Call bulkBanPartnersAction({ partnerIds, reason })
Action->>DB: Update programEnrollment.status = "banned"
Action->>Action: recordAuditLog
Action->>Enqueue: enqueueBatchJobs([{ url: Cron, body: { programId, partnerId, userId } }])
Enqueue-->>Action: enqueue response
Action-->>UI: Success toast, close modal
Enqueue->>Cron: POST /api/cron/partners/ban/process (per enrollment)
Cron->>DB: Load enrollment & related records
Cron->>DB: Transactional bulk updates (disable/expire links, reject submissions, nullify discounts, cancel payouts)
loop cancel pending commissions
Cron->>DB: fetch pending commissions (<=500)
alt found
Cron->>DB: update commissions -> status = "canceled"
else none
Cron->>Cron: break loop
end
alt error
Cron->>Cron: retry up to 3 with 1s delay
note right of Cron: log warning if any batch failed
end
end
Cron->>DB: trigger syncTotalCommissions, expire caches, mark Tinybird, queue discount deletion
Cron->>Cron: send ban notification email (if email present)
Cron-->>Enqueue: finished
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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 |
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
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: 5
🧹 Nitpick comments (9)
apps/web/app/(ee)/api/cron/partners/ban/process/cancel-commissions.ts (1)
15-61: Consider logging error details for debugging.The retry logic correctly re-attempts the same batch on failure. However, when logging the failure at line 53-55, the actual error details are discarded. Including error information would help diagnose issues in production.
if (failedBatches >= maxRetries) { console.error( - `Failed to cancel commissions after ${maxRetries} attempts. Stopping batch processing.`, + `Failed to cancel commissions after ${maxRetries} attempts. Stopping batch processing.`, + { programId, partnerId, error }, ); break; }apps/web/lib/cron/enqueue-batch-jobs.ts (1)
20-24: Consider better error serialization.
JSON.stringify(error)on anErrorobject will often produce{}sincemessageandstackare non-enumerable. Consider extracting relevant properties explicitly.} catch (error) { + const errorDetails = error instanceof Error + ? { message: error.message, stack: error.stack } + : error; console.error("[enqueueBatchJobs] Failed to enqueue batch jobs", { - error: JSON.stringify(error, null, 2), + error: errorDetails, jobs, }); await log({ - message: `[enqueueBatchJobs] Failed to enqueue batch jobs: ${JSON.stringify(error, null, 2)}`, + message: `[enqueueBatchJobs] Failed to enqueue batch jobs: ${error instanceof Error ? error.message : JSON.stringify(error)}`, type: "errors", mention: true, });apps/web/app/(ee)/api/cron/partners/ban/route.ts (3)
125-129: Sequential operations could be parallelized for better performance.
cancelCommissionsandsyncTotalCommissionsare called sequentially, butsyncTotalCommissionsdoesn't depend on the result ofcancelCommissionscompleting first. Consider parallelizing these if order doesn't matter:- // Mark the commissions as cancelled - await cancelCommissions({ - programId, - partnerId, - }); - - // Sync total commissions - await syncTotalCommissions({ - programId, - partnerId, - }); + // Mark commissions as cancelled and sync totals + await Promise.all([ + cancelCommissions({ programId, partnerId }), + syncTotalCommissions({ programId, partnerId }), + ]);
131-135: Sequential async operations could be parallelized.These cache and Tinybird operations are independent and can run in parallel:
- // Expire links from cache - await linkCache.expireMany(links); - - // Delete links from Tinybird links metadata - await recordLink(links, { deleted: true }); + // Expire links from cache and delete from Tinybird in parallel + await Promise.all([ + linkCache.expireMany(links), + recordLink(links, { deleted: true }), + ]);
192-213: Empty catch block silently swallows email errors.The empty
catch {}block on line 213 silently ignores email sending failures. Consider logging the error for debugging purposes:try { await sendEmail({ to: partner.email, subject: `You've been banned from the ${program.name} Partner Program`, variant: "notifications", replyTo: program.supportEmail || "noreply", react: PartnerBanned({ partner: { name: partner.name, email: partner.email, }, program: { name: program.name, slug: program.slug, }, - // A reason is always present because we validate the schema bannedReason: programEnrollment.bannedReason ? BAN_PARTNER_REASONS[programEnrollment.bannedReason!] : "", }), }); - } catch {} + } catch (emailError) { + console.error(`Failed to send ban email to partner ${partnerId}:`, emailError); + } }apps/web/app/(ee)/api/cron/partners/ban/process/route.ts (1)
194-215: Empty catch block silently swallows email errors.Same issue as
ban/route.ts- consider logging email failures:try { await sendEmail({ // ... email options }); - } catch {} + } catch (emailError) { + console.error(`Failed to send ban email to partner ${partnerId}:`, emailError); + }apps/web/ui/modals/bulk-ban-partners-modal.tsx (3)
42-52: Form state persists when modal is reopened.The form is not reset when the modal closes. If a user partially fills the form, closes the modal, and reopens it, they'll see the previous values. Consider resetting the form when the modal closes or when it opens:
+import { useEffect } from "react"; function BulkBanPartnersModal({ showBulkBanPartnersModal, setShowBulkBanPartnersModal, partners, onConfirm, }: BulkBanPartnersProps) { const { id: workspaceId } = useWorkspace(); const { register, handleSubmit, watch, + reset, formState: { errors }, } = useForm<BulkBanPartnersFormData>({ defaultValues: { reason: "tos_violation", confirm: "", }, }); + // Reset form when modal opens + useEffect(() => { + if (showBulkBanPartnersModal) { + reset({ reason: "tos_violation", confirm: "" }); + } + }, [showBulkBanPartnersModal, reset]);Based on learnings, when modals are cancelled, local state should be reverted to prevent UI from showing unconfirmed changes.
56-68: Error handling could be more informative.
toast.error(error.serverError)may displayundefinedif the error doesn't have aserverErrorproperty. Consider a fallback message:onError({ error }) { - toast.error(error.serverError); + toast.error(error.serverError || "Failed to ban partners. Please try again."); },
232-246:setShowBulkBanPartnersModaldoesn't need to be in useCallback dependencies.
setShowBulkBanPartnersModalis a React setState function and has a stable identity. Including it in dependencies is harmless but unnecessary:const BulkBanPartnersModalCallback = useCallback(() => { return ( <BulkBanPartnersModal showBulkBanPartnersModal={showBulkBanPartnersModal} setShowBulkBanPartnersModal={setShowBulkBanPartnersModal} partners={partners} onConfirm={onConfirm} /> ); - }, [ - showBulkBanPartnersModal, - setShowBulkBanPartnersModal, - partners, - onConfirm, - ]); + }, [showBulkBanPartnersModal, partners, onConfirm]);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
apps/web/app/(ee)/api/cron/partners/ban/process/cancel-commissions.ts(1 hunks)apps/web/app/(ee)/api/cron/partners/ban/process/route.ts(1 hunks)apps/web/app/(ee)/api/cron/partners/ban/route.ts(1 hunks)apps/web/app/(ee)/api/partners/ban/route.ts(3 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-groups-table.tsx(5 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx(4 hunks)apps/web/lib/actions/partners/ban-partner.ts(2 hunks)apps/web/lib/actions/partners/bulk-ban-partners.ts(4 hunks)apps/web/lib/cron/enqueue-batch-jobs.ts(1 hunks)apps/web/ui/modals/bulk-ban-partners-modal.tsx(1 hunks)
🧰 Additional context used
🧠 Learnings (6)
📚 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/app/(ee)/api/cron/partners/ban/process/route.tsapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-groups-table.tsxapps/web/app/(ee)/api/cron/partners/ban/route.tsapps/web/lib/actions/partners/ban-partner.tsapps/web/lib/actions/partners/bulk-ban-partners.ts
📚 Learning: 2025-10-15T01:52:37.048Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx:270-303
Timestamp: 2025-10-15T01:52:37.048Z
Learning: In React components with dropdowns or form controls that show modals for confirmation (e.g., role selection, delete confirmation), local state should be reverted to match the prop value when the modal is cancelled. This prevents the UI from showing an unconfirmed change. The solution is to either: (1) pass an onClose callback to the modal that resets the local state, or (2) observe modal visibility state and reset on close. Example context: RoleCell component in apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx where role dropdown should revert to user.role when UpdateUserModal is cancelled.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-groups-table.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsxapps/web/ui/modals/bulk-ban-partners-modal.tsx
📚 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.tsapps/web/lib/actions/partners/bulk-ban-partners.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.tsapps/web/lib/actions/partners/bulk-ban-partners.ts
📚 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/lib/actions/partners/bulk-ban-partners.ts
📚 Learning: 2025-08-14T05:17:51.825Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2735
File: apps/web/lib/actions/partners/delete-reward.ts:33-41
Timestamp: 2025-08-14T05:17:51.825Z
Learning: In the partner groups system, a rewardId can only belong to one group, establishing a one-to-one relationship between rewards and groups. This means using Prisma's `update` method (rather than `updateMany`) is appropriate when updating groups by rewardId.
Applied to files:
apps/web/lib/actions/partners/bulk-ban-partners.ts
🧬 Code graph analysis (7)
apps/web/app/(ee)/api/cron/partners/ban/process/cancel-commissions.ts (1)
packages/prisma/index.ts (1)
prisma(3-9)
apps/web/app/(ee)/api/cron/partners/ban/route.ts (12)
apps/web/lib/api/programs/get-program-enrollment-or-throw.ts (1)
getProgramEnrollmentOrThrow(6-67)packages/prisma/index.ts (1)
prisma(3-9)apps/web/app/(ee)/api/cron/partners/ban/process/cancel-commissions.ts (1)
cancelCommissions(4-71)apps/web/lib/api/partners/sync-total-commissions.ts (1)
syncTotalCommissions(5-50)apps/web/lib/api/links/cache.ts (1)
linkCache(148-148)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)apps/web/lib/api/fraud/resolve-fraud-events.ts (1)
resolveFraudEvents(6-73)apps/web/lib/api/fraud/create-fraud-events.ts (1)
createFraudEvents(6-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/errors.ts (1)
handleAndReturnErrorResponse(162-165)
apps/web/app/(ee)/api/partners/ban/route.ts (2)
apps/web/lib/api/utils.ts (1)
parseRequestBody(9-20)apps/web/lib/partners/throw-if-no-partnerid-tenantid.ts (1)
throwIfNoPartnerIdOrTenantId(5-14)
apps/web/lib/actions/partners/ban-partner.ts (5)
apps/web/lib/actions/safe-action.ts (1)
authActionClient(39-88)apps/web/lib/zod/schemas/partners.ts (1)
banPartnerSchema(754-763)apps/web/lib/openapi/partners/ban-partner.ts (1)
banPartner(6-34)packages/prisma/client.ts (1)
ProgramEnrollmentStatus(31-31)apps/web/lib/api/audit-logs/record-audit-log.ts (1)
recordAuditLog(47-73)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx (2)
apps/web/lib/types.ts (1)
EnrolledPartnerProps(459-459)apps/web/ui/modals/bulk-ban-partners-modal.tsx (1)
useBulkBanPartnersModal(222-255)
apps/web/lib/actions/partners/bulk-ban-partners.ts (3)
packages/prisma/index.ts (1)
prisma(3-9)apps/web/lib/api/audit-logs/record-audit-log.ts (1)
recordAuditLog(47-73)apps/web/lib/cron/enqueue-batch-jobs.ts (1)
enqueueBatchJobs(11-34)
apps/web/ui/modals/bulk-ban-partners-modal.tsx (5)
apps/web/lib/zod/schemas/partners.ts (2)
bulkBanPartnersSchema(769-782)BAN_PARTNER_REASONS(66-73)apps/web/lib/types.ts (1)
EnrolledPartnerProps(459-459)apps/web/lib/swr/use-workspace.ts (1)
useWorkspace(7-48)apps/web/lib/actions/partners/bulk-ban-partners.ts (1)
bulkBanPartnersAction(13-99)packages/utils/src/constants/misc.ts (1)
OG_AVATAR_URL(29-29)
🔇 Additional comments (12)
apps/web/app/(ee)/api/partners/ban/route.ts (2)
13-14: LGTM!Using
parseRequestBodyinstead of rawreq.json()provides better error handling with a clear error message for malformed JSON requests.
19-31: LGTM!The tenant-to-partner resolution logic correctly uses
findUniqueOrThrowto fail fast when the enrollment doesn't exist. ThepartnerId!coercion on line 36 is safe since the code either has apartnerIdfrom the request or assigns one after the lookup.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-groups-table.tsx (1)
85-99: LGTM!The modal hook integration and table ref pattern for resetting selection after bulk action confirmation is well-structured.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx (1)
493-509: LGTM!The conditional rendering of the Ban button based on
status !== "banned"is correct - it prevents showing the ban action when viewing already-banned partners. The selection and modal trigger logic is sound.apps/web/app/(ee)/api/cron/partners/ban/process/cancel-commissions.ts (1)
42-44: The review comment is incorrect—"canceled"(single 'l') is the correct spelling used in the Prisma schema.The CommissionStatus enum in
./packages/prisma/schema/commission.prismadefinescanceled(single 'l') at line 8, confirming that the code at lines 42-44 in the reviewed file uses the correct enum value.apps/web/lib/actions/partners/bulk-ban-partners.ts (2)
45-48: Early return behavior is appropriate for bulk operations.Silently returning when no eligible partners are found is a reasonable UX choice for bulk operations, as it prevents unnecessary errors when some selected partners are already banned.
67-98: Async processing pattern looks good.The use of
waitUntilwithPromise.allSettledensures that audit logging and batch job enqueueing happen asynchronously without blocking the response while still ensuring failures in one don't prevent the other from executing.apps/web/lib/actions/partners/ban-partner.ts (2)
15-17: Queue initialization at module scope is appropriate.The queue instance is created once at module load, which is efficient for reuse across multiple calls.
59-75: Database update correctly clears all reward and discount associations.The update properly sets the banned status, timestamp, reason, and nullifies all reward/discount references in a single atomic operation.
apps/web/app/(ee)/api/cron/partners/ban/process/route.ts (1)
125-144: Good use of parallelization for independent operations.This implementation correctly parallelizes independent operations (sync commissions, cache expiry, Tinybird recording, discount code deletion), which is more efficient than the sequential approach in the sibling
ban/route.ts.apps/web/ui/modals/bulk-ban-partners-modal.tsx (2)
106-145: Partner avatar display implementation looks good.The conditional rendering for single vs. multiple partners with stacked avatars is a nice UX touch. The use of
OG_AVATAR_URLfallback for missing images ensures consistent display.
222-255: Hook API design is clean and reusable.The
useBulkBanPartnersModalhook pattern returns both the setter and the component, making it easy to integrate into parent components. The memoization ensures stable references.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-groups-table.tsx
Show resolved
Hide resolved
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx
Show resolved
Hide resolved
…for not found and already banned scenarios
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
♻️ Duplicate comments (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx (1)
170-173: MissingonConfirmcallback to reset row selection.The hook call doesn't pass an
onConfirmcallback to reset the table selection after a successful bulk ban. After banning, the selected rows will remain visually selected even though the partners are now banned.
🧹 Nitpick comments (2)
apps/web/app/(ee)/api/partners/ban/route.ts (1)
23-42: Consider usingfindUniqueOrThrowfor consistency.The manual error handling works but adds complexity. You could achieve the same result with:
try { const programEnrollment = await prisma.programEnrollment.findUniqueOrThrow({ where: { tenantId_programId: { tenantId, programId, }, }, select: { partnerId: true, }, }); partnerId = programEnrollment.partnerId; } catch (error) { throw new DubApiError({ code: "not_found", message: `Partner with tenantId ${tenantId} not found in program.`, }); }This maintains the custom error message while reducing duplication.
apps/web/lib/actions/partners/ban-partner.ts (1)
81-108: Consider logging failures in async processing.
Promise.allSettledsilently swallows rejections from audit logging and queue enqueuing. If the queue enqueue fails, the partner will be marked as banned but side effects (disabling links, canceling commissions) will never execute.Consider adding error logging:
waitUntil( Promise.allSettled([ recordAuditLog({ /* ... */ }), queue.enqueueJSON({ /* ... */ }), ]).then((results) => { results.forEach((result, index) => { if (result.status === "rejected") { console.error( `Ban async operation ${index} failed for partner ${partnerId}:`, result.reason ); } }); }) );This provides visibility into failures without blocking the response.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/web/app/(ee)/api/cron/partners/ban/process/route.ts(1 hunks)apps/web/app/(ee)/api/partners/ban/route.ts(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx(7 hunks)apps/web/lib/actions/partners/ban-partner.ts(2 hunks)
🧰 Additional context used
🧠 Learnings (6)
📚 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/app/(ee)/api/cron/partners/ban/process/route.tsapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsxapps/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/app/(ee)/api/cron/partners/ban/process/route.tsapps/web/lib/actions/partners/ban-partner.ts
📚 Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx
📚 Learning: 2025-10-15T01:52:37.048Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx:270-303
Timestamp: 2025-10-15T01:52:37.048Z
Learning: In React components with dropdowns or form controls that show modals for confirmation (e.g., role selection, delete confirmation), local state should be reverted to match the prop value when the modal is cancelled. This prevents the UI from showing an unconfirmed change. The solution is to either: (1) pass an onClose callback to the modal that resets the local state, or (2) observe modal visibility state and reset on close. Example context: RoleCell component in apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx where role dropdown should revert to user.role when UpdateUserModal is cancelled.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx
📚 Learning: 2025-07-30T15:29:54.131Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.131Z
Learning: In apps/web/ui/partners/rewards/rewards-logic.tsx, when setting the entity field in a reward condition, dependent fields (attribute, operator, value) should be reset rather than preserved because different entities (customer vs sale) have different available attributes. Maintaining existing fields when the entity changes would create invalid state combinations and confusing UX.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx
📚 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
🧬 Code graph analysis (1)
apps/web/app/(ee)/api/partners/ban/route.ts (3)
apps/web/lib/api/utils.ts (1)
parseRequestBody(9-20)apps/web/lib/partners/throw-if-no-partnerid-tenantid.ts (1)
throwIfNoPartnerIdOrTenantId(5-14)apps/web/lib/api/errors.ts (1)
DubApiError(58-75)
⏰ 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 (20)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx (4)
13-13: LGTM!The imports are appropriate for the new bulk ban functionality and follow the existing patterns in the codebase.
Also applies to: 22-22, 60-60
494-502: LGTM!The conditional rendering correctly hides the bulk ban option when viewing already-banned partners. The callback integration properly sets the pending partners and triggers the modal.
516-516: LGTM!Modal rendering follows the established pattern alongside other modals in the component.
569-612: LGTM!The
BulkActionsMenucomponent is well-structured:
- Uses the established Command/Popover pattern consistent with
RowMenuButton- Properly extracts selected partners from the table model
- Correctly closes the popover after action selection
- Extensible design allows adding more bulk actions in the future
apps/web/app/(ee)/api/partners/ban/route.ts (2)
2-2: LGTM: Necessary imports added.The
DubApiErrorandparseRequestBodyimports support improved error handling and request validation.Also applies to: 4-4
15-15: LGTM: Improved request parsing.Using
parseRequestBodyprovides better error messages for malformed JSON requests.apps/web/lib/actions/partners/ban-partner.ts (4)
16-18: LGTM: Queue initialization.The Qstash queue instance for ban processing is correctly initialized at module level with a descriptive name.
25-25: LGTM: Reason parameter properly threaded.The
reasonparameter is correctly extracted and passed to the ban logic.Also applies to: 30-30
63-79: LGTM: Atomic ban state update.The single transaction correctly marks the enrollment as banned and clears associated rewards and discounts, ensuring an immediate consistent state before async processing begins.
110-110: LGTM: Appropriate return value.Returning the updated enrollment provides the caller with the complete banned state.
apps/web/app/(ee)/api/cron/partners/ban/process/route.ts (10)
20-36: LGTM: Proper Qstash signature verification.The endpoint correctly validates the Qstash signature before processing, ensuring only authorized queue jobs can trigger ban processing.
40-53: LGTM: Comprehensive data loading.The enrollment query includes all necessary related data (partner, links, tags, discount codes) for complete post-ban processing.
125-144: LGTM: Parallel cleanup operations.The parallel execution of cache expiration, Tinybird updates, and discount code queuing efficiently handles post-ban cleanup without unnecessary sequencing.
147-157: LGTM: Cross-program enrollment lookup.The query correctly identifies other approved program enrollments for fraud detection purposes.
159-179: LGTM: Appropriate fraud event handling.Resolving fraud events for the banned partner and creating cross-program fraud signals provides good fraud detection coverage. However, note the earlier concern about duplicate fraud events on retry.
182-216: LGTM: Email notification with error resilience.The email notification correctly uses program and partner details, and the empty catch block (line 215) ensures email failures don't block ban processing. However, note the earlier concern about duplicate emails on retry.
218-220: LGTM: Appropriate success response.The success message correctly identifies the completed ban processing.
221-228: LGTM: Proper error handling.The error handling correctly uses a type guard for the error message and logs to the cron logger before returning an error response. The previous review comment about
unknowntype handling has been addressed.
120-123: Idempotency is ensured by design—cancelCommissionsis safe for retry scenarios.The function only processes commissions with
status: "pending"(line 21). On the first execution, pending commissions are updated to "canceled". If the cron job is retried by Qstash, the function will query for pending commissions, find none (since they're now canceled), and exit gracefully without side effects. The query result on retry will be an empty array, causing the loop to break immediately (lines 32–34). No issue here.
103-111: Based on my investigation, I've found definitive evidence that the current behavior is intentional and correctly implemented:Evidence:
UI Documentation - Both ban modals explicitly state: "cancel all pending payouts" (not pending and processing)
apps/web/ui/modals/ban-partner-modal.tsx:116apps/web/ui/modals/bulk-ban-partners-modal.tsx:150Payout Status Flow - The schema shows payouts have external payment provider references (
stripeTransferId,paypalTransferId,stripePayoutId) that indicate commitment to external systemsBusiness Logic - Processing payouts are already in-flight to external payment providers (Stripe/PayPal) and cannot be easily reverted mid-transfer
Partner Protection Strategy - The ban still prevents future earnings by:
- Disabling all links
- Rejecting non-approved bounty submissions
- Removing discount associations
- Only allowing already-committed payouts to complete
Verify handling of "processing" payouts.
The code correctly implements the intended behavior: only "pending" payouts are canceled during partner bans. This matches the UI documentation which explicitly states "cancel all pending payouts." Processing payouts are deliberately left to complete because they are already committed to external payment providers (Stripe/PayPal) and cannot be reverted. The ban prevents future earnings while allowing committed payouts to fulfill.
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: 0
🧹 Nitpick comments (2)
apps/web/lib/actions/partners/ban-partner.ts (1)
81-109: Consider logging failures from Promise.allSettled.Using
Promise.allSettledprevents async failures from blocking the main flow, but queue enqueue failures will be silently dropped. Consider adding.then()handling to log rejected promises for observability.waitUntil( - Promise.allSettled([ + Promise.allSettled([ recordAuditLog({ ... }), queue.enqueueJSON({ ... }), - ]), + ]).then((results) => { + results.forEach((result, index) => { + if (result.status === "rejected") { + console.error(`Ban partner async task ${index} failed:`, result.reason); + } + }); + }), );apps/web/lib/actions/partners/bulk-ban-partners.ts (1)
45-48: Consider returning a count for UI feedback.The early return is appropriate, but the action returns
undefinedin all cases. For better UI feedback, consider returning the count of banned partners.if (programEnrollments.length === 0) { - return; + return { bannedCount: 0 }; } + + // ... after updateMany and waitUntil ... + + return { bannedCount: programEnrollments.length };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/web/lib/actions/partners/ban-partner.ts(2 hunks)apps/web/lib/actions/partners/bulk-ban-partners.ts(4 hunks)
🧰 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.tsapps/web/lib/actions/partners/bulk-ban-partners.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.tsapps/web/lib/actions/partners/bulk-ban-partners.ts
📚 Learning: 2025-08-14T05:17:51.825Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2735
File: apps/web/lib/actions/partners/delete-reward.ts:33-41
Timestamp: 2025-08-14T05:17:51.825Z
Learning: In the partner groups system, a rewardId can only belong to one group, establishing a one-to-one relationship between rewards and groups. This means using Prisma's `update` method (rather than `updateMany`) is appropriate when updating groups by rewardId.
Applied to files:
apps/web/lib/actions/partners/bulk-ban-partners.ts
🧬 Code graph analysis (1)
apps/web/lib/actions/partners/ban-partner.ts (4)
apps/web/lib/actions/safe-action.ts (1)
authActionClient(39-88)apps/web/lib/zod/schemas/partners.ts (1)
banPartnerSchema(754-763)apps/web/lib/openapi/partners/ban-partner.ts (1)
banPartner(6-34)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 (3)
16-18: LGTM on queue initialization.Module-level queue initialization is appropriate for reuse across invocations.
21-33: LGTM on action implementation.The reason parameter is correctly extracted and forwarded to the
banPartnerfunction.
111-111: Perfect. I have all the information needed. The review comment is correct - there is a genuine API contract violation. Let me provide the rewritten review comment:
API response contract mismatch: returns full enrollment object instead of documented
{ partnerId }response.The
banPartnerfunction returns the completeprogramEnrollmentUpdatedobject, but the API route atapps/web/app/(ee)/api/partners/ban/route.ts(line 52) directly returns this without transformation. However, the OpenAPI schema (apps/web/lib/openapi/partners/ban-partner.ts, lines 24-26) documents the response as{ partnerId: string }. This breaks the API contract and will fail any client expecting the documented schema. Transform the response before returning:return NextResponse.json({ partnerId: response.partnerId });⛔ Skipped due to learnings
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.apps/web/lib/actions/partners/bulk-ban-partners.ts (4)
1-11: LGTM on imports.Clean import structure with the new
enqueueBatchJobsutility for batch processing.
50-65: LGTM on bulk update logic.The update is consistent with the single-partner ban logic, properly clearing reward associations and setting the banned status.
69-84: LGTM on audit logging.The audit log entries are correctly structured and consistent with the single-partner ban action.
86-97: I'll verify the review comment by checking theenqueueBatchJobsutility implementation and its error handling. Let me start by exploring the codebase.
<function_calls>
#!/bin/bashSearch for enqueueBatchJobs utility file
fd "enqueue" --type f --extension ts --extension js | head -20
</function_calls>
<function_calls>
#!/bin/bashLook for imports in the bulk-ban-partners.ts file
rg "enqueueBatchJobs" --type ts -B2 -A2 | head -30
</function_calls>
<function_calls>
#!/bin/bashSearch for enqueueBatchJobs definition
rg "enqueueBatchJobs" --type ts -B5 -A10 | grep -E "(export|function|const)" | head -20
</function_calls>
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
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
♻️ Duplicate comments (3)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-groups-table.tsx (1)
246-254: Filter out null partners before deduplication.This issue was already flagged in a previous review.
row.original.partnercan benull(as evidenced by the null check on line 149). Mapping partners directly and then accessingp.idin theMapconstructor will throw if any selected row has a null partner.selectionControls: (tableInstance) => { // Store table reference for resetting selection tableRef.current = tableInstance; const selectedRows = tableInstance.getSelectedRowModel().rows; - const partners = selectedRows.map((row) => row.original.partner); + const partners = selectedRows + .map((row) => row.original.partner) + .filter((p): p is NonNullable<typeof p> => p != null); // Remove duplicates by partner ID const uniquePartners = Array.from( new Map(partners.map((p) => [p.id, p])).values(), ); if (uniquePartners.length === 0) return null;apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx (1)
170-173: MissingonConfirmcallback to reset row selection after bulk ban.The
useBulkBanPartnersModalhook supports anonConfirmcallback (as shown in the relevant code snippets), but it's not being passed here. After a successful bulk ban, selected rows will remain visually selected even though the partners are now banned.+ const tableRef = useRef<ReturnType<typeof useTable<EnrolledPartnerProps>>["table"] | null>(null); + const { BulkBanPartnersModal, setShowBulkBanPartnersModal } = useBulkBanPartnersModal({ partners: pendingBanPartners, + onConfirm: () => { + tableRef.current?.resetRowSelection(); + }, });You'll also need to capture the table reference in
selectionControls:selectionControls: (table) => { tableRef.current = table; return ( // ... existing JSX ); }apps/web/app/(ee)/api/cron/partners/ban/process/route.ts (1)
170-178: Potential duplicate fraud events on QStash retry.If this cron job is retried by QStash (e.g., due to timeout or transient failure),
createFraudEventswill create duplicatepartnerCrossProgramBanfraud events since there's no deduplication. TheFraudEventschema lacks a unique constraint on(programId, partnerId, type).Consider one of these approaches:
- Add
skipDuplicates: truetocreateManyincreateFraudEvents- Check for existing pending fraud events before creating
- Add a unique constraint at the database level
🧹 Nitpick comments (3)
apps/web/lib/actions/partners/bulk-ban-partners.ts (1)
45-48: Silent early return may leave callers unaware of no-op.The action returns
undefinedwhen no eligible partners are found. Consider returning an object with a count or status so the UI can provide appropriate feedback (e.g., "No partners eligible for banning").// Don't throw an error if no partners are found, just return if (programEnrollments.length === 0) { - return; + return { count: 0 }; }And at the end:
+ return { count: programEnrollments.length }; });apps/web/app/(ee)/api/cron/partners/ban/process/route.ts (1)
194-215: Silent email failure may hinder debugging.The empty
catch {}block swallows email errors without any logging. Consider adding at least a warning log to aid in troubleshooting delivery issues.try { await sendEmail({ // ... }); - } catch {} + } catch (emailError) { + console.warn( + `Failed to send ban email to partner ${partnerId}:`, + emailError instanceof Error ? emailError.message : emailError, + ); + }apps/web/ui/modals/bulk-ban-partners-modal.tsx (1)
42-52: Form state persists across modal open/close cycles.The
useFormhook initializes with default values, but these aren't reset when the modal is closed and reopened. If a user partially fills the form, closes without submitting, then reopens, the previous input may remain.Consider adding a
resetcall when the modal opens or usingkeyprop to force remount:+ // Reset form when modal opens + useEffect(() => { + if (showBulkBanPartnersModal) { + reset({ reason: "tos_violation", confirm: "" }); + } + }, [showBulkBanPartnersModal, reset]);Or in the hook:
<BulkBanPartnersModal + key={showBulkBanPartnersModal ? "open" : "closed"} showBulkBanPartnersModal={showBulkBanPartnersModal}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
apps/web/app/(ee)/api/cron/partners/ban/process/cancel-commissions.ts(1 hunks)apps/web/app/(ee)/api/cron/partners/ban/process/route.ts(1 hunks)apps/web/app/(ee)/api/partners/ban/route.ts(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-groups-table.tsx(5 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx(7 hunks)apps/web/lib/actions/partners/ban-partner.ts(2 hunks)apps/web/lib/actions/partners/bulk-ban-partners.ts(4 hunks)apps/web/lib/cron/enqueue-batch-jobs.ts(1 hunks)apps/web/ui/modals/bulk-ban-partners-modal.tsx(1 hunks)
🧰 Additional context used
🧠 Learnings (12)
📚 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/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-groups-table.tsxapps/web/app/(ee)/api/cron/partners/ban/process/route.tsapps/web/lib/actions/partners/ban-partner.tsapps/web/lib/actions/partners/bulk-ban-partners.tsapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx
📚 Learning: 2025-10-15T01:52:37.048Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx:270-303
Timestamp: 2025-10-15T01:52:37.048Z
Learning: In React components with dropdowns or form controls that show modals for confirmation (e.g., role selection, delete confirmation), local state should be reverted to match the prop value when the modal is cancelled. This prevents the UI from showing an unconfirmed change. The solution is to either: (1) pass an onClose callback to the modal that resets the local state, or (2) observe modal visibility state and reset on close. Example context: RoleCell component in apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx where role dropdown should revert to user.role when UpdateUserModal is cancelled.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-groups-table.tsxapps/web/ui/modals/bulk-ban-partners-modal.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx
📚 Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-groups-table.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx
📚 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/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-groups-table.tsxapps/web/app/(ee)/api/cron/partners/ban/process/route.tsapps/web/lib/actions/partners/ban-partner.tsapps/web/lib/actions/partners/bulk-ban-partners.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/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-groups-table.tsx
📚 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/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-groups-table.tsxapps/web/app/(ee)/api/cron/partners/ban/process/route.tsapps/web/lib/actions/partners/bulk-ban-partners.ts
📚 Learning: 2025-08-14T05:57:35.546Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2735
File: apps/web/lib/actions/partners/update-discount.ts:60-66
Timestamp: 2025-08-14T05:57:35.546Z
Learning: In the partner groups system, discounts should always belong to a group. The partnerGroup relation should never be null when updating discounts, so optional chaining on partnerGroup?.id may be unnecessary defensive programming.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-groups-table.tsx
📚 Learning: 2025-07-30T15:29:54.131Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.131Z
Learning: In apps/web/ui/partners/rewards/rewards-logic.tsx, when setting the entity field in a reward condition, dependent fields (attribute, operator, value) should be reset rather than preserved because different entities (customer vs sale) have different available attributes. Maintaining existing fields when the entity changes would create invalid state combinations and confusing UX.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-groups-table.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx
📚 Learning: 2025-11-24T08:55:31.332Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3089
File: apps/web/app/(ee)/api/fraud-rules/route.ts:71-87
Timestamp: 2025-11-24T08:55:31.332Z
Learning: In apps/web/app/(ee)/api/fraud-rules/route.ts, fraud rules cannot be created in a disabled state. When using prisma.fraudRule.upsert, the create branch intentionally omits the disabledAt field (defaulting to null, meaning enabled), while the update branch allows toggling enabled/disabled state via the disabledAt field. This is a business logic constraint.
Applied to files:
apps/web/app/(ee)/api/cron/partners/ban/process/route.ts
📚 Learning: 2025-10-06T15:48:14.205Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2935
File: apps/web/lib/actions/partners/invite-partner-from-network.ts:21-28
Timestamp: 2025-10-06T15:48:14.205Z
Learning: For the network invites limit check in apps/web/lib/actions/partners/invite-partner-from-network.ts, the team accepts that concurrent invites may bypass the limit due to race conditions. Perfect atomicity is not required for this feature.
Applied to files:
apps/web/app/(ee)/api/cron/partners/ban/process/route.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.tsapps/web/lib/actions/partners/bulk-ban-partners.ts
📚 Learning: 2025-08-14T05:17:51.825Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2735
File: apps/web/lib/actions/partners/delete-reward.ts:33-41
Timestamp: 2025-08-14T05:17:51.825Z
Learning: In the partner groups system, a rewardId can only belong to one group, establishing a one-to-one relationship between rewards and groups. This means using Prisma's `update` method (rather than `updateMany`) is appropriate when updating groups by rewardId.
Applied to files:
apps/web/lib/actions/partners/bulk-ban-partners.ts
🧬 Code graph analysis (4)
apps/web/app/(ee)/api/partners/ban/route.ts (4)
apps/web/lib/api/utils.ts (1)
parseRequestBody(9-20)apps/web/lib/partners/throw-if-no-partnerid-tenantid.ts (1)
throwIfNoPartnerIdOrTenantId(5-14)packages/prisma/index.ts (1)
prisma(3-9)apps/web/lib/api/errors.ts (1)
DubApiError(58-75)
apps/web/app/(ee)/api/cron/partners/ban/process/cancel-commissions.ts (1)
packages/prisma/index.ts (1)
prisma(3-9)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-groups-table.tsx (3)
apps/web/lib/types.ts (1)
fraudEventGroupProps(676-676)packages/ui/src/table/table.tsx (1)
useTable(51-243)apps/web/ui/modals/bulk-ban-partners-modal.tsx (1)
useBulkBanPartnersModal(222-255)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx (3)
apps/web/lib/types.ts (1)
EnrolledPartnerProps(459-459)apps/web/ui/modals/bulk-ban-partners-modal.tsx (1)
useBulkBanPartnersModal(222-255)packages/ui/src/menu-item.tsx (1)
MenuItem(43-86)
⏰ 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: api-tests
🔇 Additional comments (14)
apps/web/lib/cron/enqueue-batch-jobs.ts (1)
10-34: LGTM!The batch enqueue utility is well-implemented with comprehensive error handling. The dual logging approach (console.error for immediate debugging +
log()withmention: truefor alerting) ensures failures are visible.apps/web/lib/actions/partners/ban-partner.ts (2)
16-18: LGTM!Good use of a dedicated queue with a descriptive name for the ban-partner workflow.
81-109: Good use ofPromise.allSettledfor non-blocking side effects.This ensures both the audit log and queue enqueue are attempted independently, and neither failure blocks the other.
apps/web/app/(ee)/api/partners/ban/route.ts (1)
14-43: LGTM!Good improvements:
- Using
parseRequestBodyprovides consistent error handling for malformed JSON.- The explicit
findUnique+ manual not-found check gives better control over error messages.- Selecting only
partnerIdis efficient.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-groups-table.tsx (2)
85-100: LGTM!Good state management and modal integration. The
onConfirmcallback properly resets row selection after a successful bulk ban.
256-269: LGTM!The Ban button integration with the modal flow is clean. Setting
pendingBanPartnersbefore showing the modal ensures the modal has the correct partner list.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx (2)
569-612: Well-structured bulk actions menu component.The
BulkActionsMenucomponent follows the existing patterns in this file (similar toRowMenuButton) and correctly extracts selected partners from the table's row model before invoking the callback.
494-502: LGTM!Correctly hides the bulk ban action when viewing banned partners, preventing redundant operations.
apps/web/lib/actions/partners/bulk-ban-partners.ts (2)
86-97: Clean delegation to async processing.The batch job enqueueing with proper deduplication IDs ensures that post-ban processing (emails, cache invalidation, commission cancellation) happens asynchronously without blocking the UI, while preventing duplicate processing.
50-65: LGTM!The atomic
updateManycorrectly sets the banned status, timestamp, reason, and clears all reward/discount associations in a single database operation.apps/web/app/(ee)/api/cron/partners/ban/process/route.ts (2)
66-112: LGTM!The transactional batch update is well-structured. All four
updateManyoperations are idempotent, making this safe for potential retries.
55-59: Reasonable status guard, but doesn't prevent retry side-effects.This check correctly validates the partner is still banned before processing. However, on QStash retry, since the status remains "banned", all operations will re-execute. The
updateManyoperations are idempotent, butcreateFraudEventsandsendEmailare not.Verify whether duplicate emails could be sent on retry. If
sendEmailis not idempotent, consider tracking email delivery or using a deduplication mechanism.apps/web/ui/modals/bulk-ban-partners-modal.tsx (2)
222-255: LGTM!The hook follows the established pattern used by other modal hooks in the codebase (e.g.,
useChangeGroupModal), with proper memoization of the callback component.
94-219: Well-designed modal UI with proper safety guards.The implementation includes:
- Clear partner preview with avatar stacking for bulk selection
- Informative warning text about irreversible consequences
- Required ban reason selection
- Confirmation text input to prevent accidental bans
| let canceledCommissions = 0; | ||
| let failedBatches = 0; | ||
| const maxRetries = 3; | ||
|
|
||
| while (true) { | ||
| try { | ||
| const commissions = await prisma.commission.findMany({ | ||
| where: { | ||
| programId, | ||
| partnerId, | ||
| status: "pending", | ||
| }, | ||
| select: { | ||
| id: true, | ||
| }, | ||
| orderBy: { | ||
| id: "asc", | ||
| }, | ||
| take: 500, | ||
| }); | ||
|
|
||
| if (commissions.length === 0) { | ||
| break; | ||
| } | ||
|
|
||
| const { count } = await prisma.commission.updateMany({ | ||
| where: { | ||
| id: { | ||
| in: commissions.map((c) => c.id), | ||
| }, | ||
| }, | ||
| data: { | ||
| status: "canceled", | ||
| }, | ||
| }); | ||
|
|
||
| canceledCommissions += count; | ||
| } catch (error) { | ||
| failedBatches++; | ||
|
|
||
| // If we've failed too many times, break to avoid infinite loop | ||
| if (failedBatches >= maxRetries) { | ||
| console.error( | ||
| `Failed to cancel commissions after ${maxRetries} attempts. Stopping batch processing.`, | ||
| ); | ||
| break; | ||
| } | ||
|
|
||
| // Wait a bit before retrying the same batch | ||
| await new Promise((resolve) => setTimeout(resolve, 1000)); | ||
| } | ||
| } |
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.
Retry counter is cumulative across batches instead of per-batch.
The failedBatches counter accumulates across all batches. If batch 1 fails twice then succeeds (failedBatches = 2), and batch 2 fails once (failedBatches = 3), the loop exits even though each batch hasn't exhausted its retries. This reduces resilience for large commission sets.
let canceledCommissions = 0;
- let failedBatches = 0;
const maxRetries = 3;
while (true) {
+ let retries = 0;
+
try {
const commissions = await prisma.commission.findMany({
where: {
programId,
partnerId,
status: "pending",
},
select: {
id: true,
},
orderBy: {
id: "asc",
},
take: 500,
});
if (commissions.length === 0) {
break;
}
const { count } = await prisma.commission.updateMany({
where: {
id: {
in: commissions.map((c) => c.id),
},
},
data: {
status: "canceled",
},
});
canceledCommissions += count;
} catch (error) {
- failedBatches++;
+ retries++;
// If we've failed too many times, break to avoid infinite loop
- if (failedBatches >= maxRetries) {
+ if (retries >= maxRetries) {
console.error(
`Failed to cancel commissions after ${maxRetries} attempts. Stopping batch processing.`,
);
break;
}
// Wait a bit before retrying the same batch
await new Promise((resolve) => setTimeout(resolve, 1000));
+ continue;
}
}
- if (failedBatches > 0) {
- console.warn(
- `Cancelled ${canceledCommissions} commissions with ${failedBatches} failed batch(es).`,
- );
- } else {
- console.info(`Cancelled ${canceledCommissions} commissions.`);
- }
+ console.info(`Cancelled ${canceledCommissions} commissions.`);Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/web/app/(ee)/api/cron/partners/ban/process/cancel-commissions.ts around
lines 11 to 62, the retry counter `failedBatches` is cumulative across all
batches causing early exit; change to a per-batch retry mechanism by removing
the global `failedBatches` usage and instead implement a per-batch attempt loop
(e.g., initialize an `attempts` counter before trying to process the current
batch, retry up to `maxRetries` on failures, and only break the outer loop when
a batch succeeds or the per-batch attempts exceed `maxRetries`), or reset
`failedBatches` to 0 after a successful batch; ensure waits between retries
remain and that total canceledCommissions is incremented only on successful
updates.
Summary by CodeRabbit
New Features
Behavioral Changes
✏️ Tip: You can customize this high-level summary in your review settings.