Skip to content

Tremendous - #3952

Merged
steven-tey merged 51 commits into
mainfrom
tremendous
Jun 14, 2026
Merged

Tremendous#3952
steven-tey merged 51 commits into
mainfrom
tremendous

Conversation

@devkiran

@devkiran devkiran commented May 27, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

Release Notes

  • New Features
    • Added "Gift Cards" as a new payout method for partners, with email-based verification and delivery.
    • Introduced a Settings tab in the referrals embed for partners to configure payout preferences.
    • Implemented country eligibility restrictions for gift card payouts.
    • Added payout amount caps for gift card deliveries.

@vercel

vercel Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
dub Ready Ready Preview Jun 13, 2026 12:59am

Request Review

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces Tremendous gift card payouts as a new partner payout method. The change spans database schema extensions, API configuration, OTP email verification endpoints, referrals embed UI components with settings tab, payout execution and campaign provisioning, cron-based job queuing, eligibility rules with payout caps, and payout-method surfaces across the admin and partner-facing dashboards.

Changes

Tremendous Payout Integration

Layer / File(s) Summary
Database schema, environment, and dependency setup
packages/prisma/schema/*, apps/web/.env.example, apps/web/package.json
Prisma schema adds tremendousEmail (unique, indexed) and tremendousCampaignId to Partner and Program models, tremendousOrderId (indexed) to Payout, and tremendous to the PartnerPayoutMethod enum. Environment config adds TREMENDOUS_API_KEY placeholder; Tremendous SDK v4.12.0 is added as a dependency.
Tremendous API configuration and constants
apps/web/lib/tremendous/configuration.ts, apps/web/lib/tremendous/constants.ts, packages/utils/src/constants/tremendous-supported-countries.ts, apps/web/lib/constants/payouts-supported-countries.ts
API configuration reads TREMENDOUS_API_KEY and selects basePath by environment. Constants export enabled program IDs, maximum payout amount cap (50000 cents), prohibited top-level domains, and product IDs. Supported countries are derived by filtering the global COUNTRIES list, and are integrated into the main payout-supported-countries list.
Payout type system and state recomputation
packages/email/src/types.ts, apps/web/lib/types.ts, apps/web/lib/zod/schemas/auth.ts, apps/web/lib/payouts/recompute-partner-payout-state.ts, apps/web/lib/payouts/get-partner-payout-methods.ts
PartnerPayoutMethod type adds "tremendous"; PartnerProps gains tremendousEmail field. Email validation adds .trim() to strip whitespace. Payout state recomputation includes tremendous in method priority, evaluates tremendousEmail as an active method, and conditionally appends Tremendous to active methods. Gift Cards payout method is added to the methods list when tremendousEmail is present.
Payout method UI and surfaces
apps/web/ui/partners/payouts/payout-method-config.ts, apps/web/ui/partners/payouts/payout-method-dropdown.tsx, apps/web/ui/partners/payout-status-descriptions.ts, packages/email/src/templates/partner-payout-confirmed.tsx, apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/partner-payout-details-sheet.tsx
Payout method config adds Gift card (Tremendous) with Gift icon; dropdown renders a disabled Manage button for Tremendous with support contact tooltip. Status descriptions define payout states (pending, processing, sent, completed); payout confirmation email labels Tremendous as "Gift card"; failure tooltip instructs users to update account settings.
OTP send and verify endpoints
apps/web/app/(ee)/api/embed/referrals/tremendous/send-otp/route.ts, apps/web/app/(ee)/api/embed/referrals/tremendous/verify-otp/route.ts
Send-OTP route validates program eligibility, parses email, enforces rate limits, rejects prohibited/disposable domains, checks country and payout-method eligibility, generates OTP, persists verification token with expiry, and emails the code. Verify-OTP route validates code format, rate-limits, checks domains, looks up and validates token existence/expiry, and transactionally updates partner with email/default method/payouts-enabled timestamp on success.
Email templates for Tremendous flow
packages/email/src/templates/partner-tremendous-verify-email.tsx, packages/email/src/templates/partner-tremendous-payout.tsx
Verification email displays the OTP code, expiry minutes, and Dub branding. Payout email shows the redeem link, formatted payout amount, optional affiliate-commission period, and Footer with partner email.
Referrals embed integration and Settings tab
apps/web/app/(ee)/app.dub.co/embed/referrals/get-referrals-embed-data.ts, apps/web/app/(ee)/app.dub.co/embed/referrals/page-client.tsx, apps/web/app/(ee)/app.dub.co/embed/referrals/quickstart.tsx, apps/web/app/(ee)/app.dub.co/embed/referrals/settings.tsx
Embed data fetches tremendousEmail and defaultPayoutMethod. Page component imports Tremendous constants, computes Settings tab visibility based on program eligibility and partner country, and conditionally renders the tab. Quickstart component routes "Connect payouts" CTA to either internal Settings tab (when eligible) or external payouts page. Settings component implements two-step Gift Cards flow (email→OTP verification) and Cash option; conditionally renders based on connection state.
Tremendous payout execution and campaign provisioning
apps/web/lib/tremendous/create-tremendous-campaign.ts, apps/web/lib/tremendous/send-tremendous-payouts.ts
Campaign creation calls Tremendous API with program-derived naming and product IDs, stores campaign ID to avoid duplicates. Payout processing aggregates eligible payouts from two sources, creates Tremendous order via API, validates execution, extracts redeem URL, updates payout records with order ID, marks commissions as paid in batches, and sends partner confirmation email with redeem link.
Cron-based payout queueing orchestration
apps/web/app/(ee)/api/cron/payouts/charge-succeeded/queue-tremendous-payouts.ts, apps/web/app/(ee)/api/cron/payouts/charge-succeeded/route.ts, apps/web/app/(ee)/api/cron/payouts/send-tremendous-payout/route.ts
Queue module groups eligible processing payouts by partner, chunks into batches of 100, and enqueues Qstash jobs with deduplication. Charge-succeeded handler includes queue call in concurrent batch. Send endpoint validates and invokes payout processing.
Payout eligibility caps and validation
apps/web/lib/api/payouts/payout-eligibility-filter.ts, apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx
Eligibility filter excludes Tremendous payouts exceeding the cap. Payout table applies cap to batch-confirm checks and displays ineligibility tooltip with formatted max amount and instruction to connect alternative methods.
Stripe webhook partner data
apps/web/app/(ee)/api/stripe/connect/v2/webhook/recipient-account-closed.ts, apps/web/app/(ee)/api/stripe/connect/v2/webhook/recipient-configuration-updated.ts, apps/web/app/(ee)/api/stripe/connect/webhook/account-application-deauthorized.ts, apps/web/app/(ee)/api/stripe/connect/webhook/account-updated.ts, apps/web/app/(ee)/partners.dub.co/(onboarding)/onboarding/payouts/page.tsx
Stripe deauthorization and configuration webhooks include tremendousEmail in partner fetches so that payout-state recomputation can evaluate Tremendous as active during account changes. Onboarding payouts page also fetches the field.
Force withdrawal with Tremendous support
apps/web/lib/actions/partners/force-withdrawal.ts
Force withdrawal validation accepts "tremendous" method; action branch calls sendTremendousPayouts with forceWithdrawal: true when the method is selected.
Disposable and Tremendous domain sync cron
apps/web/app/(ee)/api/cron/disposable-emails/route.ts
Endpoint is refactored to use withCron wrapper; fetches disposable-domain list and Tremendous prohibited domains in parallel; normalizes and atomically syncs to Redis via temporary sets.
Stripe transfer conditional payout selection
apps/web/lib/partners/create-stripe-transfer.ts
Current invoice payout query now branches on invoiceId presence, defaulting to empty list when absent.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • dubinc/dub#3973: Both PRs modify apps/web/lib/payouts/recompute-partner-payout-state.ts to extend payout method handling—this PR adds Tremendous as an active method, while that PR introduces a hasPayoutStateChanged flag that may interact with the recomputation logic.
  • dubinc/dub#3927: Both PRs modify the same payout state recomputation function; this PR adds Tremendous to the priority and active-method evaluation, while that PR changes the default-selection and return logic.
  • dubinc/dub#3732: Both PRs modify payout eligibility in payout-table.tsx—this PR adds Tremendous cap-based ineligibility checks, while that PR refactors the batch-confirm selection from selectedPayoutId to selectedPayoutIds.

Suggested reviewers

  • pepeladeira
  • steven-tey

🐰 Through the lens of gift cards and tokens we see,
Tremendous payouts flow seamlessly free,
OTP gates, campaigns take flight,
Embed tabs and crons align just right!
Partners now choose their rewards with glee.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.12% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Tremendous' is vague and generic, using a single non-descriptive term that does not convey meaningful information about the changeset. Use a more specific title that summarizes the main change, such as 'Add Tremendous gift card payout integration' or 'Integrate Tremendous for partner payouts'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tremendous

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

❤️ Share

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

@steven-tey

Copy link
Copy Markdown
Collaborator

@coderabbitai final review, any bugs/breaking changes?

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== withCron implementation ==="
rg "withCron" --type ts -n -C5 apps/web/lib 2>/dev/null | head -80

echo "=== old disposable-emails route for reference ==="
git diff HEAD~1 -- "apps/web/app/(ee)/api/cron/disposable-emails/route.ts" 2>/dev/null | head -100

echo "=== current disposable-emails route ==="
cat "apps/web/app/(ee)/api/cron/disposable-emails/route.ts" 2>/dev/null

echo "=== forceWithdrawalAction before change ==="
git log --oneline -5 -- "apps/web/lib/actions/partners/force-withdrawal.ts" 2>/dev/null

echo "=== current force-withdrawal ==="
cat "apps/web/lib/actions/partners/force-withdrawal.ts" 2>/dev/null

echo "=== check all callers of recomputePartnerPayoutState for tremendousEmail ==="
rg "recomputePartnerPayoutState" --type ts -n -B5 -A10 | head -200

echo "=== verify-otp P2025 handling ==="
cat "apps/web/app/(ee)/api/embed/referrals/tremendous/verify-otp/route.ts" 2>/dev/null

echo "=== send-tremendous-payouts cast ==="
rg "CreateOrder200Response" --type ts -n -C5

echo "=== tremendousEnv configuration ==="
cat "apps/web/lib/tremendous/configuration.ts" 2>/dev/null

echo "=== check if any callers of recomputePartnerPayoutState miss tremendousEmail ==="
rg "recomputePartnerPayoutState" --type ts -n -l

Length of output: 34648


🏁 Script executed:

#!/bin/bash
echo "=== withCron full implementation ==="
cat "apps/web/lib/cron/with-cron.ts" 2>/dev/null

echo "=== set-default-payout-method.ts full ==="
cat "apps/web/lib/actions/partners/set-default-payout-method.ts" 2>/dev/null

echo "=== forceWithdrawal - check for sendTremendousPayouts ==="
rg "sendTremendousPayouts" --type ts -n -C5

echo "=== paypal callback full partner select ==="
cat "apps/web/app/(ee)/api/paypal/callback/route.ts" 2>/dev/null | head -100

echo "=== tremendousEmail in set-default-payout-method ==="
rg "tremendousEmail" apps/web/lib/actions/partners --type ts -n -C3

echo "=== the authPartnerActionClient ctx partner select ==="
rg "authPartnerActionClient" --type ts -n -C3 apps/web/lib/auth | head -60

echo "=== partnerUserMiddleware or partner select in safe-action ==="
fd "safe-action.ts" apps/web/lib --exec cat {}

Length of output: 14997



@steven-tey Here's the focused bug/breaking-change summary after digging through the actual implementations:


✅ Previously flagged items that are already handled

  • P2025 race condition — the try/catch wrapping the Prisma transaction in verify-otp/route.ts is already there and correctly maps P2025bad_request. No issue.
  • withCron authwithCron calls verifyQstashSignature for every POST, so the rewritten disposable-emails cron is properly secured.
  • recomputePartnerPayoutState callers missing tremendousEmailauthPartnerActionClient in safe-action.ts uses prisma.partner.findFirst without a select (full object), so tremendousEmail is always present in the partner ctx.

🔴 Real bug: forceWithdrawal silently does nothing for Tremendous partners

forceWithdrawalAction now passes validation for "tremendous" payout method, but the underlying forceWithdrawal function has no branch for it:

// force-withdrawal.ts
if (partner.defaultPayoutMethod === "stablecoin") {
  await createStablecoinPayout({ ... });
} else if (partner.defaultPayoutMethod === "connect") {
  await createStripeTransfer({ ... });
}
// ← no branch for "tremendous"

A Tremendous partner who clicks "Force Withdrawal" will: pass validation → acquire the Redis lock → have nothing happen → lock is released. No error, no payout, no feedback. The fix is to add:

} else if (partner.defaultPayoutMethod === "tremendous") {
  await sendTremendousPayouts({
    partnerId: partner.id,
    forceWithdrawal: true,
  });
}

🟡 Unsafe Tremendous API response cast

const { order } = data as CreateOrder200Response; at line 138 of send-tremendous-payouts.ts remains. If the Tremendous API returns an unexpected response shape (e.g. a rate-limit or auth error body), this cast will produce a confusing runtime crash when accessing order.status or order.rewards?.[0]. At minimum, add a null check on order before the status assertion.


⚠️ Deployment: 3 DB migrations must run before go-live

These new Prisma fields will cause runtime failures if the schema migration hasn't run:

Model New field
Partner tremendousEmail String? @unique``
Program tremendousCampaignId String?
Payout tremendousOrderId String?

Ensure the migration is applied before the new code is deployed.


TL;DR: The force-withdrawal silent no-op is the only concrete functional bug. The unsafe API cast is a latent crash risk. Everything else previously flagged is already handled correctly.

@steven-tey

Copy link
Copy Markdown
Collaborator

@coderabbitai addressed your feedback, thank you! Final review please 🙏

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

@steven-tey Thanks for addressing the feedback! Triggering a final full review now 🙏

(ミ・ω・ミ)

✅ Action performed

Full review finished.

@steven-tey

Copy link
Copy Markdown
Collaborator

@coderabbitai also, any breaking changes to existing non-Tremendous/giftcard payout flows?

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== recomputePartnerPayoutState - full implementation ==="
cat "apps/web/lib/payouts/recompute-partner-payout-state.ts" 2>/dev/null

echo "=== get-partner-payout-methods.ts - full ==="
cat "apps/web/lib/payouts/get-partner-payout-methods.ts" 2>/dev/null

echo "=== payout-eligibility-filter.ts ==="
cat "apps/web/lib/api/payouts/payout-eligibility-filter.ts" 2>/dev/null

echo "=== charge-succeeded route ==="
cat "apps/web/app/(ee)/api/cron/payouts/charge-succeeded/route.ts" 2>/dev/null

echo "=== force-withdrawal.ts full ==="
cat "apps/web/lib/actions/partners/force-withdrawal.ts" 2>/dev/null

echo "=== ALL callers of recomputePartnerPayoutState - check selects ==="
rg "recomputePartnerPayoutState" --type ts -n -l

echo "=== recipient-account-closed.ts ==="
cat "apps/web/app/(ee)/api/stripe/connect/v2/webhook/recipient-account-closed.ts" 2>/dev/null

echo "=== recipient-configuration-updated.ts ==="
cat "apps/web/app/(ee)/api/stripe/connect/v2/webhook/recipient-configuration-updated.ts" 2>/dev/null

echo "=== account-application-deauthorized.ts ==="
cat "apps/web/app/(ee)/api/stripe/connect/webhook/account-application-deauthorized.ts" 2>/dev/null

echo "=== account-updated.ts ==="
cat "apps/web/app/(ee)/api/stripe/connect/webhook/account-updated.ts" 2>/dev/null

echo "=== partner onboarding payouts page ==="
cat "apps/web/app/(ee)/partners.dub.co/(onboarding)/onboarding/payouts/page.tsx" 2>/dev/null

echo "=== set-default-payout-method ==="
cat "apps/web/lib/actions/partners/set-default-payout-method.ts" 2>/dev/null

echo "=== all other places that call recomputePartnerPayoutState ==="
rg "recomputePartnerPayoutState" --type ts -n -A5 -B10

echo "=== queue-tremendous-payouts - qstash batch errors handling ==="
cat "apps/web/app/(ee)/api/cron/payouts/charge-succeeded/queue-tremendous-payouts.ts" 2>/dev/null

echo "=== any calls to forceWithdrawal ==="
rg "forceWithdrawal" --type ts -n -C5 apps/web 2>/dev/null | head -100

Length of output: 66085



@steven-tey Good news — no breaking changes to existing Connect, PayPal, or Stablecoin flows. Here's a quick rundown of everything checked:

Area Verdict Reasoning
recomputePartnerPayoutState callers ✅ Safe All 5 callers (account-updated, account-application-deauthorized, recipient-account-closed, recipient-configuration-updated, paypal/callback) now include tremendousEmail in their Prisma selects. The set-default-payout-method action uses authPartnerActionClient which returns the full partner object.
payout-eligibility-filter.ts NOT clause ✅ Safe Prisma evaluates NOT: { A, B } as NOT(A AND B). For Connect/PayPal/Stablecoin partners defaultPayoutMethod ≠ "tremendous" so the first condition is false → NOT(false AND anything) = true → they always pass through.
charge-succeeded route ✅ Safe queueTremendousPayouts is added to the existing Promise.allSettled([...]) batch, so a Tremendous failure cannot block Stripe, PayPal, or External payout queuing.
force-withdrawal ✅ Fixed The tremendous branch is now present — this was the bug flagged in the previous review and has been addressed.
PAYOUT_METHOD_PRIORITY ordering ✅ Safe Tremendous is last (stablecoin > connect > paypal > tremendous), so it will never displace an existing partner's default method.
getPartnerPayoutMethods ✅ Safe Tremendous only appears as an option when partner.tremendousEmail is set, so it's invisible to all non-Tremendous partners.

Everything looks good. Existing payout flows are fully isolated from the Tremendous integration. 🐇✨

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (4)
apps/web/app/(ee)/api/embed/referrals/tremendous/verify-otp/route.ts (1)

188-197: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Narrow Prisma error before reading .code.

The catch block accesses error.code without first verifying the error type. While the project's tsconfig may not enforce strict catch variable typing, the established codebase pattern is to narrow with instanceof Prisma.PrismaClientKnownRequestError before accessing .code. This prevents potential runtime issues if a different error type is thrown.

🔧 Proposed fix

Add the import near existing Prisma imports:

 import { prisma } from "`@dub/prisma`";
+import { Prisma } from "`@dub/prisma/client`";

Update the catch block:

-    } catch (error) {
-      if (error.code === "P2025") {
+    } catch (error) {
+      if (
+        error instanceof Prisma.PrismaClientKnownRequestError &&
+        error.code === "P2025"
+      ) {
         throw new DubApiError({
           code: "bad_request",
           message: "You already have a payout method connected.",
         });
       }

       throw error;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/`(ee)/api/embed/referrals/tremendous/verify-otp/route.ts around
lines 188 - 197, The catch block in route.ts reads error.code without narrowing
the error; import Prisma from '`@prisma/client`' (near the other Prisma imports)
and change the handler so it first checks error instanceof
Prisma.PrismaClientKnownRequestError before testing error.code === "P2025", and
only then throw the DubApiError (same payload), otherwise rethrow the original
error.
apps/web/app/(ee)/api/cron/disposable-emails/route.ts (1)

30-41: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate HTTP response status before parsing body.

Promise.allSettled marks a fetch as "fulfilled" even when the HTTP response is 4xx/5xx (fetch only rejects on network failures). The current code parses res.value.text() without checking res.value.ok, which could sync error page content into Redis if the remote server returns an error status.

🛡️ Proposed fix
   const disposableDomains =
-    disposableRes.status === "fulfilled"
+    disposableRes.status === "fulfilled" && disposableRes.value.ok
       ? (await disposableRes.value.text()).split("\n").filter(Boolean)
       : [];

   const tremendousDomains =
-    tremendousRes.status === "fulfilled"
+    tremendousRes.status === "fulfilled" && tremendousRes.value.ok
       ? (await tremendousRes.value.text())
           .split("\n")
           .map((d) => d.trim().toLowerCase())
           .filter(Boolean)
       : [];

Optionally, add warnings for non-ok responses:

+  if (disposableRes.status === "fulfilled" && !disposableRes.value.ok) {
+    console.warn(
+      `Disposable domains fetch returned HTTP ${disposableRes.value.status}: ${disposableRes.value.statusText}`,
+    );
+  }
+
+  if (tremendousRes.status === "fulfilled" && !tremendousRes.value.ok) {
+    console.warn(
+      `Tremendous prohibited domains fetch returned HTTP ${tremendousRes.value.status}: ${tremendousRes.value.statusText}`,
+    );
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/`(ee)/api/cron/disposable-emails/route.ts around lines 30 - 41,
The code parses response bodies from the Promise.allSettled results
(disposableRes and tremendousRes) without checking HTTP status, so replace
direct awaits of res.value.text() with a guard that verifies res.value.ok first
(for both disposableRes and tremendousRes), skip or return an empty array when
!res.value.ok, and optionally log a warning including the status and URL; ensure
you still call .text() only after confirming res.value.ok to avoid storing error
pages in Redis.
apps/web/app/(ee)/app.dub.co/embed/referrals/settings.tsx (1)

40-55: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the disabled-tooltip trigger keyboard focusable.

At Line 46, the tooltip child is a plain div, so keyboard users can’t focus it to discover why the action is unavailable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/`(ee)/app.dub.co/embed/referrals/settings.tsx around lines 40 -
55, The tooltip content wrapper is a plain div so keyboard users can’t focus it;
update the Tooltip child (the element rendering {text} inside the
disabledTooltip branch) to be keyboard-focusable by replacing the div or adding
attributes: give it tabIndex={0}, role="button" and aria-disabled="true" (keep
existing className via cn and the same visual styles), so keyboard users can tab
to the disabled control and discover the tooltip; ensure you still render {text}
and preserve className, cn, and the surrounding Tooltip props.
apps/web/lib/tremendous/send-tremendous-payouts.ts (1)

171-187: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add runtime guard for data.order before destructuring.

The cast data as CreateOrder200Response relies on an unchecked assertion. If the API returns an unexpected response, accessing order.status or order.rewards will throw a TypeError. Add a guard to fail gracefully with a clear error message.

This echoes the prior review comment on lines 171-187 which remains unaddressed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/tremendous/send-tremendous-payouts.ts` around lines 171 - 187,
Add a runtime guard that verifies data.order exists before destructuring: don't
directly do const { order } = data as CreateOrder200Response; instead check if
data && (data as CreateOrder200Response).order (or use 'if (!data || !("order"
in data) || !(data as any).order)') and if missing call console.error with a
clear message and await markPayoutsAsProcessed(currentInvoicePayouts) then
return; keep the existing flow (redeemUrl, status checks) intact and update
references to use the guarded order variable (order, reward, redeemUrl) to avoid
TypeError when the API returns an unexpected shape.
🧹 Nitpick comments (3)
packages/prisma/schema/partner.prisma (1)

73-73: 🚀 Performance & Scalability | ⚡ Quick win

Remove the redundant index on tremendousEmail.

Line 73 already defines tremendousEmail as @unique, which creates an index. Keeping Line 122 (@@index(tremendousEmail)) duplicates indexing and adds unnecessary write/index maintenance overhead.

Also applies to: 122-122

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/prisma/schema/partner.prisma` at line 73, Remove the redundant
explicit index for tremendousEmail: since the tremendousEmail field is already
declared with `@unique` in the model (tremendousEmail String? `@unique`), delete the
separate model-level index declaration @@index(tremendousEmail) so the unique
constraint remains the sole index and avoids duplicate indexing overhead.
apps/web/lib/payouts/recompute-partner-payout-state.ts (1)

15-18: 📐 Maintainability & Code Quality | ⚡ Quick win

Update the priority-order doc comment to include Tremendous.

The comment still says fallback order is stablecoin > connect > paypal, but runtime priority now also includes tremendous. Keeping this stale makes future debugging/reviews harder.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/payouts/recompute-partner-payout-state.ts` around lines 15 - 18,
Update the stale doc comment in recompute-partner-payout-state (the
function/module that "Computes payoutsEnabledAt and defaultPayoutMethod") to
reflect the current runtime fallback priority by adding "tremendous" to the
list; change the fallback order text from "stablecoin > connect > paypal" to
"stablecoin > connect > tremendous > paypal" so the comment matches actual
behavior. Ensure the updated comment sits above the recomputePartnerPayoutState
implementation and preserves the existing phrasing about preserving the
partner's existing default when still active.
apps/web/lib/tremendous/send-tremendous-payouts.ts (1)

251-269: 🩺 Stability & Availability | 💤 Low value

Consider logging Promise.allSettled rejections.

waitUntil(Promise.allSettled([...])) silently discards any rejected results from trackCommissionStatusUpdatesByProgram or enqueueBatchJobs. While enqueueBatchJobs logs internally before throwing, rejected promises won't surface to callers. Consider inspecting the settled results and logging failures.

Note: This echoes the prior review comment about waitUntil swallowing rejections.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/tremendous/send-tremendous-payouts.ts` around lines 251 - 269,
The call to waitUntil(Promise.allSettled([...])) swallows rejected outcomes from
trackCommissionStatusUpdatesByProgram and enqueueBatchJobs; update the code to
await Promise.allSettled, inspect the returned results array, log any failures
(including the reason and which task failed) for traceability, and if any
settled result is a rejection rethrow or return a rejected error so waitUntil
sees the failure; specifically modify the block using Promise.allSettled with
trackCommissionStatusUpdatesByProgram and enqueueBatchJobs to iterate results,
log details for result.status === "rejected", and throw a combined Error (or the
first rejection) to surface the error.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/app/`(ee)/app.dub.co/embed/referrals/page-client.tsx:
- Around line 201-210: The current country eligibility check (computed by
isTremendousCountrySupported and used by showSettingsTab) incorrectly treats
partner.country === null as unsupported; change the predicate from
Boolean(partner.country &&
TREMENDOUS_SUPPORTED_COUNTRIES.includes(partner.country)) to treat null as
allowed by using (!partner.country ||
TREMENDOUS_SUPPORTED_COUNTRIES.includes(partner.country)). Update the same logic
in both page-client.tsx (isTremendousCountrySupported / showSettingsTab) and
quickstart.tsx so the OTP-permitted null-country cases are allowed while still
excluding explicitly unsupported countries.

In `@apps/web/lib/tremendous/create-tremendous-campaign.ts`:
- Line 27: The code is unsafely casting Tremendous responses (e.g., data as
CreateCampaign200Response in create-tremendous-campaign.ts and data as
CreateOrder200Response in send-tremendous-payouts.ts) and then accessing nested
props like campaign.id, order.status, and order.rewards; add runtime validation
before accessing these fields: check that data is an object, that data.campaign
or data.order exists and is an object, and that required fields (id, status,
rewards) have the expected types/structures; if validation fails, return or
throw a clear error (or handle the failure path) instead of proceeding. Use
lightweight inline guards (typeof/Array.isArray/null checks) or a schema
validator (zod/io-ts) to validate the response shape and replace the unsafe
casts in the functions that process the Tremendous API response.
- Around line 27-36: Before updating the DB ensure the API response actually
contains a campaign id: validate that data (the CreateCampaign200Response) has a
non-null campaign object with a defined id before calling prisma.program.update;
if missing, throw or handle the error path (e.g., log and abort) instead of
writing null/undefined to tremendousCampaignId. Locate the use of data as
CreateCampaign200Response and the prisma.program.update call (referenced by
program.id and tremendousCampaignId) and add a runtime guard that checks
data?.campaign?.id (or equivalent) and only calls prisma.program.update when it
is present, otherwise handle the failure case.

In `@apps/web/lib/tremendous/send-tremendous-payouts.ts`:
- Around line 240-248: The error log inside the catch of sendTremendousPayouts
incorrectly references `[createStripeTransfer]`; update the log message in the
catch block that calls log(...) to reference `[sendTremendousPayouts]` (or a
similarly accurate tag) and keep the existing context including
payoutIds.join(", ") and error.message so the commission-update failure is
correctly attributed to sendTremendousPayouts.

---

Duplicate comments:
In `@apps/web/app/`(ee)/api/cron/disposable-emails/route.ts:
- Around line 30-41: The code parses response bodies from the Promise.allSettled
results (disposableRes and tremendousRes) without checking HTTP status, so
replace direct awaits of res.value.text() with a guard that verifies
res.value.ok first (for both disposableRes and tremendousRes), skip or return an
empty array when !res.value.ok, and optionally log a warning including the
status and URL; ensure you still call .text() only after confirming res.value.ok
to avoid storing error pages in Redis.

In `@apps/web/app/`(ee)/api/embed/referrals/tremendous/verify-otp/route.ts:
- Around line 188-197: The catch block in route.ts reads error.code without
narrowing the error; import Prisma from '`@prisma/client`' (near the other Prisma
imports) and change the handler so it first checks error instanceof
Prisma.PrismaClientKnownRequestError before testing error.code === "P2025", and
only then throw the DubApiError (same payload), otherwise rethrow the original
error.

In `@apps/web/app/`(ee)/app.dub.co/embed/referrals/settings.tsx:
- Around line 40-55: The tooltip content wrapper is a plain div so keyboard
users can’t focus it; update the Tooltip child (the element rendering {text}
inside the disabledTooltip branch) to be keyboard-focusable by replacing the div
or adding attributes: give it tabIndex={0}, role="button" and
aria-disabled="true" (keep existing className via cn and the same visual
styles), so keyboard users can tab to the disabled control and discover the
tooltip; ensure you still render {text} and preserve className, cn, and the
surrounding Tooltip props.

In `@apps/web/lib/tremendous/send-tremendous-payouts.ts`:
- Around line 171-187: Add a runtime guard that verifies data.order exists
before destructuring: don't directly do const { order } = data as
CreateOrder200Response; instead check if data && (data as
CreateOrder200Response).order (or use 'if (!data || !("order" in data) || !(data
as any).order)') and if missing call console.error with a clear message and
await markPayoutsAsProcessed(currentInvoicePayouts) then return; keep the
existing flow (redeemUrl, status checks) intact and update references to use the
guarded order variable (order, reward, redeemUrl) to avoid TypeError when the
API returns an unexpected shape.

---

Nitpick comments:
In `@apps/web/lib/payouts/recompute-partner-payout-state.ts`:
- Around line 15-18: Update the stale doc comment in
recompute-partner-payout-state (the function/module that "Computes
payoutsEnabledAt and defaultPayoutMethod") to reflect the current runtime
fallback priority by adding "tremendous" to the list; change the fallback order
text from "stablecoin > connect > paypal" to "stablecoin > connect > tremendous
> paypal" so the comment matches actual behavior. Ensure the updated comment
sits above the recomputePartnerPayoutState implementation and preserves the
existing phrasing about preserving the partner's existing default when still
active.

In `@apps/web/lib/tremendous/send-tremendous-payouts.ts`:
- Around line 251-269: The call to waitUntil(Promise.allSettled([...])) swallows
rejected outcomes from trackCommissionStatusUpdatesByProgram and
enqueueBatchJobs; update the code to await Promise.allSettled, inspect the
returned results array, log any failures (including the reason and which task
failed) for traceability, and if any settled result is a rejection rethrow or
return a rejected error so waitUntil sees the failure; specifically modify the
block using Promise.allSettled with trackCommissionStatusUpdatesByProgram and
enqueueBatchJobs to iterate results, log details for result.status ===
"rejected", and throw a combined Error (or the first rejection) to surface the
error.

In `@packages/prisma/schema/partner.prisma`:
- Line 73: Remove the redundant explicit index for tremendousEmail: since the
tremendousEmail field is already declared with `@unique` in the model
(tremendousEmail String? `@unique`), delete the separate model-level index
declaration @@index(tremendousEmail) so the unique constraint remains the sole
index and avoids duplicate indexing overhead.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 11074f23-4789-45af-a1d5-a17c5510699a

📥 Commits

Reviewing files that changed from the base of the PR and between 0a6a17e and 95ff569.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (45)
  • apps/web/.env.example
  • apps/web/app/(ee)/api/cron/disposable-emails/route.ts
  • apps/web/app/(ee)/api/cron/payouts/charge-succeeded/queue-tremendous-payouts.ts
  • apps/web/app/(ee)/api/cron/payouts/charge-succeeded/route.ts
  • apps/web/app/(ee)/api/cron/payouts/send-tremendous-payout/route.ts
  • apps/web/app/(ee)/api/embed/referrals/tremendous/send-otp/route.ts
  • apps/web/app/(ee)/api/embed/referrals/tremendous/verify-otp/route.ts
  • apps/web/app/(ee)/api/stripe/connect/v2/webhook/recipient-account-closed.ts
  • apps/web/app/(ee)/api/stripe/connect/v2/webhook/recipient-configuration-updated.ts
  • apps/web/app/(ee)/api/stripe/connect/webhook/account-application-deauthorized.ts
  • apps/web/app/(ee)/api/stripe/connect/webhook/account-updated.ts
  • apps/web/app/(ee)/app.dub.co/embed/referrals/get-referrals-embed-data.ts
  • apps/web/app/(ee)/app.dub.co/embed/referrals/page-client.tsx
  • apps/web/app/(ee)/app.dub.co/embed/referrals/quickstart.tsx
  • apps/web/app/(ee)/app.dub.co/embed/referrals/settings.tsx
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/partner-payout-details-sheet.tsx
  • apps/web/app/(ee)/partners.dub.co/(onboarding)/onboarding/payouts/page.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/webhooks/[webhookId]/page-client.tsx
  • apps/web/lib/actions/partners/force-withdrawal.ts
  • apps/web/lib/api/payouts/payout-eligibility-filter.ts
  • apps/web/lib/constants/payouts-supported-countries.ts
  • apps/web/lib/partners/create-stripe-transfer.ts
  • apps/web/lib/payouts/get-partner-payout-methods.ts
  • apps/web/lib/payouts/recompute-partner-payout-state.ts
  • apps/web/lib/tremendous/configuration.ts
  • apps/web/lib/tremendous/constants.ts
  • apps/web/lib/tremendous/create-tremendous-campaign.ts
  • apps/web/lib/tremendous/send-tremendous-payouts.ts
  • apps/web/lib/types.ts
  • apps/web/lib/zod/schemas/auth.ts
  • apps/web/package.json
  • apps/web/playwright/workspaces/billing-trial.spec.ts
  • apps/web/ui/partners/payout-status-descriptions.ts
  • apps/web/ui/partners/payouts/payout-method-config.ts
  • apps/web/ui/partners/payouts/payout-method-dropdown.tsx
  • packages/email/src/templates/partner-payout-confirmed.tsx
  • packages/email/src/templates/partner-tremendous-payout.tsx
  • packages/email/src/templates/partner-tremendous-verify-email.tsx
  • packages/email/src/types.ts
  • packages/prisma/schema/partner.prisma
  • packages/prisma/schema/payout.prisma
  • packages/prisma/schema/program.prisma
  • packages/utils/src/constants/index.ts
  • packages/utils/src/constants/tremendous-supported-countries.ts

Comment thread apps/web/app/(ee)/app.dub.co/embed/referrals/page-client.tsx
Comment thread apps/web/lib/tremendous/create-tremendous-campaign.ts
Comment thread apps/web/lib/tremendous/create-tremendous-campaign.ts
Comment thread apps/web/lib/tremendous/send-tremendous-payouts.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants