-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Deduplicate lead commission creation #2832
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.
|
WalkthroughBranch commission creation by event (lead vs sale), unify first-commission lookup by event, add lead deduplication and sale reward/duration checks with status propagation, split earnings calc by event, remove Reward.maxAmount, rename/manual-wire create-manual-commission action, update several button sizes/labels and simplify partners page render. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant E as Event (lead/sale/click)
participant S as createPartnerCommission
participant DB as DB (Commissions/Rewards)
E->>S: send {eventType, rewardId, quantity, amount, partner, link}
S->>DB: find firstCommission by (partner, link, eventType)
alt firstCommission exists
alt eventType == lead
S->>S: log "lead dedup" and skip creation
else
S->>DB: if rewardId ≠ firstCommission.rewardId fetch originalReward
S->>S: check reward.maxDuration / originalReward.maxDuration
alt maxDuration === 0 or expired
S->>S: log & skip creation
else
S->>S: if firstCommission.status ∈ {fraud,canceled} -> propagate status
end
end
end
alt proceed to create
alt eventType == lead
S->>S: earnings = reward.amount * quantity
else
S->>S: earnings = calculateSaleEarnings({reward, sale:{quantity, amount}})
end
S->>DB: insert commission (rewardId, earnings, status)
DB-->>S: created
else
S-->>E: no commission created
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Pre-merge checks (2 passed, 1 warning)❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
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: 1
🧹 Nitpick comments (1)
apps/web/lib/partners/create-partner-commission.ts (1)
177-183: Propagated fraud/canceled status: also suppress notifications/workflows.You set status = firstCommission.status, but later notifications and workflows only gate on isClawback. We should avoid notifying or triggering workflows for fraud/canceled commissions.
Outside this hunk, refine these flags:
- const isClawback = earnings < 0; - const shouldTriggerWorkflow = !isClawback && !skipWorkflow; + const isClawback = earnings < 0; + const isInvalidStatus = status === "fraud" || status === "canceled"; + const shouldTriggerWorkflow = !isClawback && !isInvalidStatus && !skipWorkflow;And guard notifications similarly:
- !isClawback && + !isClawback && !isInvalidStatus && notifyPartnerCommission({ /* ... */ })
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/web/lib/partners/create-partner-commission.ts(2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Vade Review
- GitHub Check: build
🔇 Additional comments (4)
apps/web/lib/partners/create-partner-commission.ts (4)
89-91: LGTM: click earnings fast-path is clear and correct.Simple and deterministic; no dedup or duration concerns for clicks.
115-121: Lead dedup early-return is correct, contingent on the fixed lookup scope.Once the query is properly scoped by programId and enforced customerId, this return prevents duplicate lead payouts as intended.
Please confirm leads are intended to be “one per partner/customer per program.” If not, we should key dedup by a stronger event identity.
187-196: LGTM: per-event earnings branching is correct.Lead = fixed amount × quantity; sale uses calculateSaleEarnings; aligns with reward semantics.
153-169: Use event timestamp (createdAt) for maxDuration gating — handle backfillsFile: apps/web/lib/partners/create-partner-commission.ts (lines 153-169)
Use the event's timestamp when computing monthsDifference; fall back to now if createdAt is absent.
- const monthsDifference = differenceInMonths( - new Date(), - firstCommission.createdAt, - ); + const eventDate = createdAt ?? new Date(); + const monthsDifference = differenceInMonths( + eventDate, + firstCommission.createdAt, + );If maxDuration semantics are “N whole months inclusive starting from the first commission,” >= is appropriate; otherwise consider > and document the rule.
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
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
apps/web/lib/partners/create-partner-commission.ts (1)
200-226: Make dedupe idempotent under concurrency (handle P2002 + add unique keys)findFirst→create is racy. Add DB uniqueness and treat duplicates as no-ops.
try { - const commission = await prisma.commission.create({ + const commission = await prisma.commission.create({ data: { id: createId({ prefix: "cm_" }), programId, partnerId, rewardId: reward?.id, customerId, linkId, eventId, invoiceId, userId: user?.id, quantity, amount, type: event, currency, earnings, status, description, createdAt, }, include: { customer: true, }, }); @@ - } catch (error) { - console.error("Error creating commission", error); + } catch (error: any) { + // Idempotency: swallow unique violations (e.g., same event/invoice) + if (error && error.code === "P2002") { + console.warn("Duplicate commission detected (unique constraint). No-op."); + return; + } + console.error("Error creating commission", error);Recommended DB indexes (migration examples):
- Lead dedupe (one lead per partner/customer/program):
CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS commissions_unique_lead_per_customer ON "Commission" ("programId","partnerId","customerId") WHERE type = 'lead';
- Event-level idempotency (sales/leads based on eventId):
CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS commissions_unique_program_event ON "Commission" ("programId","eventId") WHERE "eventId" IS NOT NULL;Also applies to: 315-326
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/create-commission-sheet.tsx (2)
98-103: Clear leadEventDate is handled; also clear leadEventName when toggle is offWithout clearing, a previously set name will be submitted even when the toggle is disabled.
useEffect(() => { if (!hasCustomLeadEventDate) { setValue("leadEventDate", null); } }, [hasCustomLeadEventDate, setValue]); + + useEffect(() => { + if (!hasCustomLeadEventName) { + setValue("leadEventName", null); + } + }, [hasCustomLeadEventName, setValue]);
601-606: Invoice/Product toggles are hidden by default—user can’t enable themThe entire block (including the Switch) is hidden when
hasInvoiceId/hasProductIdis false, leaving no way to reveal the inputs. Remove the conditionalclassName/styleon the container; keep the input itself gated by the boolean.<AnimatedSizeContainer height transition={{ ease: "easeInOut", duration: 0.2 }} - className={!hasInvoiceId ? "hidden" : ""} - style={{ display: !hasInvoiceId ? "none" : "block" }} ><AnimatedSizeContainer height transition={{ ease: "easeInOut", duration: 0.2 }} - className={!hasProductId ? "hidden" : ""} - style={{ display: !hasProductId ? "none" : "block" }} >Also applies to: 656-661
♻️ Duplicate comments (1)
apps/web/lib/partners/create-partner-commission.ts (1)
89-104: Fix dedupe scope and require customerId for lead/saleThe firstCommission query omits programId and uses a possibly undefined customerId, which Prisma ignores — causing cross-program/cross-customer dedupe and status bleed-through. Add a guard and scope the query.
- } else { + } else { + // Require customerId for lead/sale to avoid cross-customer dedupe/status bleed-through + if ((event === "lead" || event === "sale") && !customerId) { + await log({ + message: `Missing customerId for ${event} commission (program ${programId}, partner ${partnerId}) — skipping.`, + type: "errors", + mention: true, + }); + return; + } const firstCommission = await prisma.commission.findFirst({ where: { + programId, partnerId, - customerId, + customerId: customerId!, // guaranteed by guard above type: event, },
🧹 Nitpick comments (6)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-popover-buttons.tsx (1)
22-39: Minor a11y: expose popover state to screen readersConsider passing aria-expanded/aria-haspopup via Button or Popover so assistive tech reflects the open state.
- <Button + <Button onClick={() => setOpenPopover(!openPopover)} variant="secondary" className="h-8 w-auto px-1.5 sm:h-9" icon={<ThreeDots className="h-5 w-5 text-neutral-500" />} + aria-haspopup="menu" + aria-expanded={openPopover} />Also applies to: 42-58, 61-71
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/import-export-buttons.tsx (1)
32-38: Minor a11y: reflect popover stateSame note as other popover—add aria-expanded/aria-haspopup on the trigger.
- <Button + <Button onClick={() => setOpenPopover(!openPopover)} variant="secondary" className="h-8 w-auto px-1.5 sm:h-9" icon={<ThreeDots className="h-5 w-5 text-neutral-500" />} + aria-haspopup="menu" + aria-expanded={openPopover} />Also applies to: 66-82, 85-95
apps/web/lib/partners/create-partner-commission.ts (1)
58-61: Nit: guard/validation for quantity and currency (non-blocking)Optionally validate quantity > 0 and default currency for sale/custom to reduce bad writes.
- let earnings = 0; + let earnings = 0; let reward: RewardProps | null = null; let status: CommissionStatus = "pending"; + if (quantity <= 0) { + await log({ message: `Invalid quantity (${quantity}) for ${event}`, type: "errors" }); + return; + }Also applies to: 200-221
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/create-commission-button.tsx (1)
7-7: Ensure accessible name and avoid mobile ambiguityOn mobile the visible label is just “Create”, which is ambiguous for screen readers and can cause a tiny hydration/layout shift when
isMobileflips. Add a stable accessible name (and optional title).<Button type="button" onClick={() => setShowCreateCommissionSheet(true)} text={`Create${isMobile ? "" : " commission"}`} + aria-label="Create commission" + title="Create commission" shortcut="C" className="h-8 px-3 sm:h-9" />Also applies to: 22-25
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/create-commission-sheet.tsx (2)
164-175: Don’t treat 0 as “missing” for numeric fieldsCurrent checks disable submit when amount/saleAmount is 0. If 0 is a valid value per schema (min: 0), compare against null/undefined instead.
if (commissionType === "custom") { - return !amount; + return amount == null; } if (commissionType === "sale") { - return !linkId || !customerId || !saleAmount; + return !linkId || !customerId || saleAmount == null; } if (commissionType === "lead") { return !linkId || !customerId; }
616-622: Microcopy: prefer “Invoice ID” / “Product ID” casingTiny polish for consistency with labels below.
- <span className="rounded-md border border-neutral-200 bg-neutral-100 px-1 py-0.5 text-xs"> - invoiceID - </span> + <span className="rounded-md border border-neutral-200 bg-neutral-100 px-1 py-0.5 text-xs"> + Invoice ID + </span>- <span className="rounded-md border border-neutral-200 bg-neutral-100 px-1 py-0.5 text-xs"> - productID - </span> + <span className="rounded-md border border-neutral-200 bg-neutral-100 px-1 py-0.5 text-xs"> + Product ID + </span>Also applies to: 671-676
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/create-bounty-button.tsx(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-popover-buttons.tsx(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/create-commission-button.tsx(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/create-commission-sheet.tsx(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/create-group-button.tsx(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/import-export-buttons.tsx(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-button.tsx(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/page-client.tsx(0 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/page.tsx(2 hunks)apps/web/lib/actions/partners/create-manual-commission.ts(1 hunks)apps/web/lib/partners/create-partner-commission.ts(2 hunks)apps/web/lib/zod/schemas/rewards.ts(0 hunks)packages/prisma/schema/reward.prisma(0 hunks)
💤 Files with no reviewable changes (3)
- packages/prisma/schema/reward.prisma
- apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/page-client.tsx
- apps/web/lib/zod/schemas/rewards.ts
🧰 Additional context used
🧬 Code graph analysis (2)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/create-commission-sheet.tsx (1)
apps/web/lib/actions/partners/create-manual-commission.ts (1)
createManualCommissionAction(23-318)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/page.tsx (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx (1)
PartnersTable(91-440)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Vade Review
- GitHub Check: build
🔇 Additional comments (10)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-popover-buttons.tsx (1)
68-70: LGTM: consistent button sizingThe responsive height tweak aligns with the broader UI pattern. No behavioral change.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/import-export-buttons.tsx (1)
92-94: LGTM: consistent button sizingMatches the standardized h-8/sm:h-9 pattern.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/page.tsx (1)
5-5: Replace wrapper with PartnersTable — confirmed client boundaryPartnersTable contains "use client" at apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx:1; safe to replace the wrapper.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-button.tsx (1)
3-3: LGTM: mobile-aware label + sizingClear pattern reuse with useMediaQuery and height classes; shortcut remains intact.
Also applies to: 7-7, 19-22
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/create-bounty-button.tsx (1)
3-3: LGTM: responsive text and sizingConsistent with other action buttons; no functional changes.
Also applies to: 7-7, 20-23
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/create-group-button.tsx (1)
6-6: LGTM: responsive label and heightReads well; conditional shortcut binding is a nice touch.
Also applies to: 10-10, 41-43
apps/web/lib/actions/partners/create-manual-commission.ts (2)
188-206: Workflow duplication check — no duplicate commission triggers foundOnly WorkflowTrigger.commissionEarned is invoked in apps/web/lib/partners/create-partner-commission.ts; apps/web/lib/actions/partners/create-manual-commission.ts emits WorkflowTrigger.leadRecorded / .saleRecorded but does not itself emit commissionEarned — manual path will produce commissionEarned only via createPartnerCommission.
23-24: Rename acknowledged — verify downstream imports updatedSearch for "createCommissionAction" returned no matches (empty output). Re-run a repo-wide search and confirm no lingering imports; update any occurrences to createManualCommissionAction. Example command: rg -n --hidden -S '\bcreateCommissionAction\b' .
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/create-commission-sheet.tsx (2)
116-126: LGTM: action wiring switched to createManualCommissionActionHook usage and success/error handlers remain compatible.
1-1: Import rename verified — no stale callers remain.
No matches for createCommissionAction; apps/web/lib/actions/partners/create-manual-commission.ts exports createManualCommissionAction (line 23).
|
@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: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (10)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/create-group-button.tsx (1)
26-33: Bug: disabled state bypassed via keyboard shortcuthandleCreateGroup opens the sheet when disabled (counts undefined) because the else branch still runs; the hotkey remains active. Guard early.
const handleCreateGroup = () => { - if (!disabled && groupsCount >= groupsLimit) - setShowPartnersUpgradeModal(true); - else setShowCreateGroupSheet(true); + if (disabled) return; + if (groupsCount >= groupsLimit) { + setShowPartnersUpgradeModal(true); + } else { + setShowCreateGroupSheet(true); + } };Optionally register/unregister the shortcut based on disabled if the hook supports deps.
apps/web/lib/actions/partners/create-manual-commission.ts (3)
118-131: Scope existing lead to the same link/partner or fall backReusing the first lead for a customer without checking link/partner can mis-attribute analytics and commissions.
- if ( - !leadEventDate && - !leadEventName && - existingLeadEvent && - existingLeadEvent.data.length > 0 - ) { - leadEvent = leadEventSchemaTB.parse(existingLeadEvent.data[0]); + if ( + !leadEventDate && + !leadEventName && + existingLeadEvent && + existingLeadEvent.data.length > 0 + ) { + const candidate = leadEventSchemaTB.parse(existingLeadEvent.data[0]); + // Require same link to avoid cross-partner attribution + if (candidate.link_id === linkId) { + leadEvent = candidate; + // If the customer isn't linked yet, attach based on existing event + if (!customer.linkId) { + shouldUpdateCustomer = true; + clickEvent = { + ...candidate, + click_id: candidate.click_id, + } as ClickEventTB; + } + } + // else: let the dummy flow create a scoped click+lead below } else {
215-229: Don’t hardcode USD; accept currency from input and propagateAllow currency to be provided, defaulting to workspace/program currency if available.
- payment_processor: "custom", - currency: "usd", + payment_processor: "custom", + currency: parsedInput.currency ?? "usd",- currency: "usd", + currency: parsedInput.currency ?? "usd",If schema doesn’t yet accept currency, extend createCommissionSchema accordingly.
Also applies to: 232-244
301-305: clickedAt expects a Date — convert ISO stringPrisma DateTime generally expects a Date; you’re passing an ISO string from TB.
- clickedAt: clickEvent?.timestamp, + clickedAt: clickEvent?.timestamp + ? new Date(clickEvent.timestamp) + : undefined,apps/web/lib/partners/create-partner-commission.ts (2)
226-228: Avoid logging full commission payload (PII risk)JSON stringifying the included customer leaks PII to logs. Log only essentials.
- console.log( - `Created a ${event} commission ${commission.id} (${currencyFormatter(commission.earnings)}) for ${partnerId}: ${JSON.stringify(commission)}`, - ); + console.log( + `Created ${event} commission ${commission.id} for partner ${partnerId}, customer ${commission.customerId}, earnings ${currencyFormatter(commission.earnings)}` + );
200-221: Race-proof with DB uniqueness and duplicate handlingfindFirst → create is TOCTOU; enforce idempotency in DB and swallow duplicates.
- Leads: unique per (programId, partnerId, customerId) where type='lead'.
- Sales: unique on eventId (or invoiceId+programId if applicable).
Migration (Postgres):
CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS commissions_unique_lead_per_customer ON "Commission" ("programId","partnerId","customerId") WHERE type = 'lead'; CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS commissions_unique_event_id ON "Commission" ("eventId") WHERE "eventId" IS NOT NULL;Then catch Prisma P2002 on create() to no-op.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/create-commission-sheet.tsx (4)
438-446: Switch is hidden initially due to container height=0 (cannot be toggled).The AnimatedSizeContainer collapses the entire block to 0px when
hasCustomLeadEventDateis false, hiding the Switch and making it impossible to enable. Keep the switch visible; only animate the extra input.Minimal fix to keep the switch visible:
- <AnimatedSizeContainer - height - transition={{ ease: "easeInOut", duration: 0.2 }} - style={{ - height: hasCustomLeadEventDate ? "auto" : "0px", - overflow: "hidden", - }} - > + <AnimatedSizeContainer + height + transition={{ ease: "easeInOut", duration: 0.2 }} + style={{ height: "auto", overflow: "hidden" }} + >The actual input is already gated by
hasCustomLeadEventDate && (...), so only the date picker appears when enabled.
486-489: Same issue for “custom lead event name” toggle.Container collapses and hides the Switch. Keep container height auto.
- <AnimatedSizeContainer - height - transition={{ ease: "easeInOut", duration: 0.2 }} - style={{ - height: hasCustomLeadEventName ? "auto" : "0px", - overflow: "hidden", - }} - > + <AnimatedSizeContainer + height + transition={{ ease: "easeInOut", duration: 0.2 }} + style={{ height: "auto", overflow: "hidden" }} + >
604-606: Invoice ID switch also hidden by CSS.Hiding the whole container with
hidden/display:noneprevents enabling the switch. Remove these and gate only the input field.- className={!hasInvoiceId ? "hidden" : ""} - style={{ display: !hasInvoiceId ? "none" : "block" }}
659-661: Same issue for Product ID toggle.Remove container hiding; keep only the input conditional.
- className={!hasProductId ? "hidden" : ""} - style={{ display: !hasProductId ? "none" : "block" }}
♻️ Duplicate comments (2)
apps/web/lib/partners/create-partner-commission.ts (2)
98-104: Dedup scope missing programId and customer guard for lead/sale
- Missing programId can dedupe across programs.
- If customerId is undefined, Prisma ignores it, causing cross-customer dedupe/status bleed.
Apply both guard and scoping:
} else { + // Require a customer for lead/sale to avoid cross-customer dedupe/propagation + if ((event === "lead" || event === "sale") && !customerId) { + await log({ + message: `Missing customerId for ${event} commission (program ${programId}, partner ${partnerId}) — skipping.`, + type: "errors", + mention: true, + }); + return; + } const firstCommission = await prisma.commission.findFirst({ where: { + programId, partnerId, - customerId, + customerId: customerId!, // guarded above type: event, },
163-166: Use event/create timestamp for duration window, not “now”Backfills/manuals get misclassified when using new Date().
- const monthsDifference = differenceInMonths( - new Date(), - firstCommission.createdAt, - ); + const effectiveCreatedAt = createdAt ?? new Date(); + const monthsDifference = differenceInMonths( + effectiveCreatedAt, + firstCommission.createdAt, + );
🧹 Nitpick comments (8)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/import-export-buttons.tsx (2)
89-94: Add accessible name to icon-only triggerIcon-only buttons need an aria-label for screen readers.
- <Button + <Button onClick={() => setOpenPopover(!openPopover))} variant="secondary" className="h-8 w-auto px-1.5 sm:h-9" icon={<ThreeDots className="h-5 w-5 text-neutral-500" />} + aria-label="More partner actions" />
70-76: Explicit button type to avoid unintended form submitsIf this popover sits inside a form in the future, default type="submit" can be problematic.
- <button + <button onClick={() => { setOpenPopover(false); setShowExportPartnersModal(true); }} className="w-full rounded-md p-2 hover:bg-neutral-100 active:bg-neutral-200" + type="button" >- <button + <button onClick={onClick} className="w-full rounded-md p-2 hover:bg-neutral-100 active:bg-neutral-200" + type="button" >Also applies to: 108-114
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-popover-buttons.tsx (2)
65-71: Add accessible name to icon-only triggerProvide aria-label for better a11y.
- <Button + <Button onClick={() => setOpenPopover(!openPopover)} variant="secondary" className="h-8 w-auto px-1.5 sm:h-9" icon={<ThreeDots className="h-5 w-5 text-neutral-500" />} + aria-label="More commission actions" />
26-37: Explicit button type to avoid accidental submitsHarden inner buttons with type="button".
- <button + <button onClick={() => { setOpenPopover(false); setClawbackSheetOpen(true); }} className="w-full rounded-md p-2 hover:bg-neutral-100 active:bg-neutral-200" + type="button" >- <button + <button onClick={() => { setOpenPopover(false); setShowExportCommissionsModal(true); }} className="w-full rounded-md p-2 hover:bg-neutral-100 active:bg-neutral-200" + type="button" >Also applies to: 46-57
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/create-commission-button.tsx (2)
3-7: Avoid hydration flicker from responsive label.useMediaQuery can change after mount, briefly flipping text between "Create commission" and "Create." Gate on a mounted flag if you want to avoid the flash. Optional, but improves polish.
Example:
+import { useEffect, useState } from "react"; ... export function CreateCommissionButton() { - const { isMobile } = useMediaQuery(); + const { isMobile } = useMediaQuery(); + const [mounted, setMounted] = useState(false); + useEffect(() => setMounted(true), []);Then use
mounted && isMobilewhere needed.
20-25: Preserve descriptive labeling on mobile and hide shortcut badge."Create" alone is ambiguous for screen readers. Add an aria-label and suppress the keyboard shortcut hint on mobile.
<Button type="button" onClick={() => setShowCreateCommissionSheet(true)} - text={`Create${isMobile ? "" : " commission"}`} - shortcut="C" + text={`Create${isMobile ? "" : " commission"}`} + aria-label="Create commission" + shortcut={isMobile ? undefined : "C"} className="h-8 px-3 sm:h-9" />apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/create-commission-sheet.tsx (2)
116-126: Harden error handling for next-safe-action.
error.serverErrormay be undefined. Provide a safe fallback and optionally surface validation errors.- const { executeAsync, isPending } = useAction(createManualCommissionAction, { + const { executeAsync, isPending } = useAction(createManualCommissionAction, { onSuccess: async () => { toast.success("A commission has been created for the partner!"); setIsOpen(false); await mutatePrefix(`/api/commissions?workspaceId=${workspaceId}`); }, - onError({ error }) { - console.log(error); - toast.error(error.serverError); + onError({ error }) { + console.log(error); + const msg = + error.serverError ?? + (error.validationErrors + ? "Please check the highlighted fields." + : "Something went wrong. Please try again."); + toast.error(msg); }, });
565-595: Align money input UX with Amount field.Add
onKeyDown={handleMoneyKeyDown}tosaleAmountto prevent invalid characters (e/E,+,-), matching the “Amount” field behavior.render={({ field }) => ( <input {...field} type="number" onWheel={(e) => e.currentTarget.blur()} + onKeyDown={handleMoneyKeyDown}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/create-bounty-button.tsx(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-popover-buttons.tsx(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/create-commission-button.tsx(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/create-commission-sheet.tsx(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/create-group-button.tsx(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/import-export-buttons.tsx(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-button.tsx(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/page-client.tsx(0 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/page.tsx(2 hunks)apps/web/lib/actions/partners/create-manual-commission.ts(1 hunks)apps/web/lib/partners/create-partner-commission.ts(2 hunks)apps/web/lib/zod/schemas/rewards.ts(0 hunks)packages/prisma/schema/reward.prisma(0 hunks)
💤 Files with no reviewable changes (3)
- apps/web/lib/zod/schemas/rewards.ts
- packages/prisma/schema/reward.prisma
- apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/page-client.tsx
🔇 Additional comments (5)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/page.tsx (1)
5-5: PartnersTable is a client component — safe to import into a server pageapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx begins with "use client" and uses client-only APIs (useState, useEffect, useSWR, next/navigation, window.confirm); no SSR-only/Node APIs were found in this file.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-button.tsx (1)
3-3: LGTM: responsive label + sizingMobile-aware label and height look good; keyboard shortcut wiring unchanged.
Also applies to: 7-7, 19-22
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/create-group-button.tsx (1)
41-43: LGTM: responsive label + sizingConsistent with other buttons in this PR.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/create-bounty-button.tsx (1)
3-3: LGTM: mobile-aware label + sizingNo functional changes; shortcut still “C”.
Also applies to: 7-7, 20-23
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/create-commission-sheet.tsx (1)
1-1: Import rename looks right — verify no stale callers remainrg returned no matches — unable to confirm removal. Re-run from repo root: rg -nP '\bcreateCommissionAction\b|actions/partners/create-commission' or manually confirm all imports/usages were removed/replaced.
Summary by CodeRabbit
New Features
Bug Fixes