Skip to content

Add workflows/create-partner-commission#3961

Merged
steven-tey merged 27 commits into
mainfrom
create-partner-commission-workflow
Jun 2, 2026
Merged

Add workflows/create-partner-commission#3961
steven-tey merged 27 commits into
mainfrom
create-partner-commission-workflow

Conversation

@steven-tey

@steven-tey steven-tey commented May 29, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added a queued partner-commission workflow endpoint and QStash-backed labeled job triggering with retries.
  • Refactor

    • Commission creation moved to async/queued processing across sales, leads, manual, clawback, bounty, Shopify, and backfill flows.
    • Fraud checks decoupled from immediate commission creation; side effects run asynchronously.
    • Added input validation for commission requests and coercion of reward date inputs.
    • Increased Stripe invoice import limit from 12 to 36.
  • Tests

    • Shared fraud-event verification helper and longer commission polling timeout.

@steven-tey steven-tey self-assigned this May 29, 2026
@vercel

vercel Bot commented May 29, 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 1, 2026 11:07pm

Request Review

@coderabbitai

coderabbitai Bot commented May 29, 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 moves partner commission creation to queued Upstash (QStash) workflows, adds a create-partner-commission workflow route that computes eligibility and creates commissions, replaces legacy trigger/logger helpers with triggerQStashWorkflow and getWorkflowConfig, introduces a Zod schema and exported type for commission queue requests, and updates many callsites to enqueue commission creation and consume queued results.

Changes

Async Partner Commission Workflow Refactor

Layer / File(s) Summary
Workflow trigger helper & QStash client
apps/web/lib/cron/qstash-workflow.ts, apps/web/lib/cron/index.ts
Replace triggerWorkflows with triggerQStashWorkflow (typed workflowType/workflowLabel, retries, default flowControl), add getWorkflowConfig, and make QStash client baseUrl configurable via QSTASH_URL.
Schemas, types, small utils
apps/web/lib/zod/schemas/commissions.ts, apps/web/lib/zod/schemas/rewards.ts, apps/web/lib/types.ts, apps/web/app/(ee)/api/cron/utils.ts
Add createPartnerCommissionSchema and exported CreatePartnerCommissionProps; change reward context date fields to z.coerce.date().nullish(); add small logAndReturn helper.
New create-partner-commission workflow route
apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts
Add Upstash Workflow route that validates input, runs stepCreateCommission (earnings/reward resolution, dedupe/eligibility, Prisma create with retry) and stepRunSideEffects (webhook/postback/sync/audit, conditional fraud detection), and optionally links bounty submissions.
Partner-approved workflow updates
apps/web/app/(ee)/api/workflows/partner-approved/route.ts
Parse requests with new inputSchema, remove per-step structured workflow logger usage in favor of console/shared logger, and expand failureFunction to emit structured workflow.failed with correlation from getWorkflowConfig.
Queueing implementation & callsite migrations
apps/web/lib/partners/queue-partner-commission-creation.ts, apps/web/lib/api/partners/applications/approve-partner.ts, apps/web/lib/actions/partners/bulk-approve-partners.ts, apps/web/app/(ee)/api/partner-profile/referrals/route.ts
Add queuePartnerCommissionCreation which enqueues the create-partner-commission workflow and returns partner/links/webhookPartner. Migrate partner approval/bulk flows to triggerQStashWorkflow with workflowType/workflowLabel.
Conversion & webhook handlers
apps/web/lib/api/conversions/track-lead.ts, apps/web/lib/api/conversions/track-sale.ts, apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts, apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts
Queue partner commissions (pass clickEvent, isFirstConversion), remove post-sync fraud-detection paths that depended on synchronous commission objects, and use result?.webhookPartner when emitting lead.created/sale.created webhooks. Sale tracking adds publishWorkspaceClicksUsageEvent.
Actions, integrations, bounty, backfill
apps/web/lib/actions/partners/create-manual-commission.ts, apps/web/lib/actions/partners/create-clawback.ts, apps/web/lib/bounty/api/approve-bounty-submission.ts, apps/web/lib/integrations/shopify/*, apps/web/scripts/programs/backfill-reuse-commission.ts
Migrate manual commissions, clawbacks, bounty approvals, Shopify integrations, and backfill scripts to call queuePartnerCommissionCreation, pass userId instead of user, and update logging/counting to reflect queued commissions.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • dubinc/dub#3804: Related Shopify discount attribution and commission wiring changes.
  • dubinc/dub#3749: Related partner-approved workflow route modifications.
  • dubinc/dub#3957: Related commission creation and earnings/skip logic for workflow step.

Suggested reviewers

  • devkiran

Poem

🐰 I queued the hops and labeled each run,

Upstash carries the work — one by one,
Clicks and leads now wait their turn in line,
Webhooks read webhookPartner from the queued sign,
A rabbit cheers the async switch — hooray, well done!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add workflows/create-partner-commission' directly and specifically describes the main addition in the PR—a new workflow route for partner commission creation.
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 create-partner-commission-workflow

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.

@devkiran devkiran marked this pull request as ready for review May 29, 2026 16:30

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/lib/actions/partners/create-manual-commission.ts (1)

250-274: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Inconsistent payload: user object instead of userId.

In the useExistingEvents path, the commission payload passes the full user object (line 269), but other paths in this file use userId: user.id. This inconsistency may cause validation or processing issues in the workflow.

🐛 Proposed fix
         commissionsToCreate.push(
           ...saleEventData.map((saleEvent) => ({
             event: "sale" as const,
             programId,
             partnerId,
             linkId: link.id,
             customerId: customer.id,
             eventId: saleEvent.event_id,
             quantity: 1,
             amount: saleEvent.amount,
             currency: saleEvent.currency,
             invoiceId: saleEvent.invoice_id,
             createdAt: new Date(saleEvent.timestamp),
             // if the invoice payment was refunded on Stripe, set the commission status to refunded as well
             ...(stripeCustomerInvoices.find(
               (invoice) => invoice.id === saleEvent.invoice_id,
             )?.refunded && {
               status: "refunded",
             }),
-            user,
+            userId: user.id,
             context: {
               customer: { country: customer.country },
             },
           })),
         );
🤖 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/actions/partners/create-manual-commission.ts` around lines 250 -
274, The saleEvent mapping that pushes to commissionsToCreate is passing the
full user object (property user) instead of the expected userId; change the
payload created in the saleEventData.map (the object inside
commissionsToCreate.push) to remove the user property and add userId: user.id so
it matches the other code paths and validation expectations (ensure the key is
exactly userId and not user to keep consistency with the rest of the commission
creation logic).
🧹 Nitpick comments (6)
apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts (4)

315-338: ⚡ Quick win

Avoid recalculating subscription duration.

Lines 326-329 recalculate subscriptionDurationMonths using the same logic already performed at lines 223-228 and stored in the context object (line 235). This duplication is unnecessary and may introduce inconsistencies if the calculations diverge.

♻️ Proposed refactor
           // Recurring sale reward (maxDuration > 0)
           else {
-            const subscriptionDurationMonths = differenceInMonths(
-              createdAt ?? new Date(), // account for custom commission creation date
-              firstCommission.createdAt,
-            );
+            const subscriptionDurationMonths = context?.customer?.subscriptionDurationMonths ?? 0;

             if (subscriptionDurationMonths >= reward.maxDuration) {
🤖 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/workflows/create-partner-commission/route.ts around
lines 315 - 338, The subscription duration is being recalculated in the block
that checks reward.maxDuration; instead of recomputing differenceInMonths, use
the previously computed value stored on the context
(context.subscriptionDurationMonths) to avoid duplication and drift. Replace the
local subscriptionDurationMonths calculation in the maxDuration branch with a
read of context.subscriptionDurationMonths (and fallback to 0 or a safe default
if undefined), leaving the existing logAndReturn calls, and keep references to
reward, firstCommission, createdAt, partnerId, and event unchanged.

501-534: ⚡ Quick win

Consider logging individual side-effect failures.

Promise.allSettled (lines 501-534) prevents individual failures from aborting the workflow, which is appropriate for side effects. However, failures are currently silent. Consider logging rejected promises to aid debugging.

♻️ Proposed enhancement
- await Promise.allSettled([
+ const results = await Promise.allSettled([
   sendWorkspaceWebhook({
     ...
   }),
   ...
 ]);
+
+ results.forEach((result, index) => {
+   if (result.status === 'rejected') {
+     console.error(`Side effect ${index} failed:`, result.reason);
+   }
+ });
🤖 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/workflows/create-partner-commission/route.ts around
lines 501 - 534, The Promise.allSettled call silences failures from
sendWorkspaceWebhook, sendPartnerPostback, syncTotalCommissions, and
notifyPartnerCommission; after awaiting Promise.allSettled([...]) inspect the
returned results array and log any rejected entries with contextual info
(include the function name, partnerId, programId, workspace id, and the
rejection reason) so individual side-effect failures are visible; use the unique
call sites (sendWorkspaceWebhook, sendPartnerPostback, syncTotalCommissions,
notifyPartnerCommission) to map results to their source when creating the log
message.

166-186: ⚡ Quick win

Rename context parameter to avoid shadowing.

Line 183: The destructured context variable shadows the context parameter from the Upstash workflow (line 69). While the workflow context is not used inside stepCreateCommission, this shadowing reduces code clarity.

♻️ Proposed refactor
 async function stepCreateCommission(
   input: StepFunctionInput,
 ): Promise<StepCreateCommissionOutput> {
   let {
     event,
     partnerId,
     programId,
     linkId,
     customerId,
     eventId,
     invoiceId,
     amount,
     quantity,
     currency,
     description,
     createdAt,
     userId,
-    context,
+    context: rewardContext,
     programEnrollment,
   } = input;

Then update references to contextrewardContext throughout the function (lines 231, 233, 237, 249).

🤖 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/workflows/create-partner-commission/route.ts around
lines 166 - 186, The destructured variable named context inside the
stepCreateCommission function is shadowing the Upstash workflow context
parameter; rename the destructured context to rewardContext and update all uses
inside stepCreateCommission (e.g., where context is accessed later in the
function) to rewardContext so the workflow-level context is no longer shadowed
and clarity is preserved; ensure you update every reference (places currently
using context within stepCreateCommission such as the reward handling/creation
code) to the new rewardContext identifier.

385-385: 💤 Low value

Clarify the TODO comment about createdAt.

Line 385 includes a TODO: Check this comment for the conditional createdAt spread. If this is a known issue or requires verification, consider documenting what needs to be checked or opening a follow-up issue to track it.

Do you want me to open a new issue to track this TODO, or can you clarify what needs to be checked here?

🤖 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/workflows/create-partner-commission/route.ts at line
385, The TODO on the conditional spread "(...(createdAt && { createdAt }))"
needs clarification: decide whether createdAt should be accepted from the
request or always set server-side, then update the code and comment accordingly
— if createdAt must be client-controlled, add explicit validation in the
createPartnerCommission handler (validate format/allowed range) and keep the
spread with a clear comment; if it must be server-assigned, remove the spread
and set createdAt when constructing the model (e.g., in the
createPartnerCommission function) and delete the TODO; if you can’t decide now,
replace the TODO with a one-line TODO linked to a freshly opened issue tracking
whether createdAt may be supplied by clients and add a brief test covering both
behaviors.
apps/web/lib/cron/qstash-workflow.ts (1)

79-111: ⚡ Quick win

Consider adding type-safe accessors for correlation fields.

getWorkflowConfig accesses body fields (e.g., body.programId, body.partnerId) without type checking. Since body is typed as Record<string, unknown>, these accesses are not type-safe and could reference missing or mistyped fields.

Consider defining input schemas for each workflow type and using them to validate/parse the body before extracting correlation data.

🤖 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/cron/qstash-workflow.ts` around lines 79 - 111,
getWorkflowConfig currently reads properties off body (typed Record<string,
unknown>) unsafely; add type-safe accessors by defining per-workflow input
schemas (e.g., zod schemas or TypeScript runtime validators) for each
workflowType (like "partner-approved" and "create-partner-commission"),
validate/parse body inside getWorkflowConfig (or a helper like
parsePartnerApprovedBody / parseCreatePartnerCommissionBody) and then extract
strongly-typed fields into the correlation object; ensure on validation failure
you handle/throw a clear error or return an empty correlation consistently so
downstream code sees only validated, typed values.
apps/web/lib/partners/create-partner-commission.ts (1)

39-389: ⚡ Quick win

Remove the large commented-out legacy block before merge.

Lines 39–389 are the old createPartnerCommission body left as comments. This logic now lives in the workflow route, so keeping ~350 lines of dead code here hurts readability and risks drift. Git history preserves it if needed.

🤖 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/partners/create-partner-commission.ts` around lines 39 - 389,
Remove the large commented-out legacy implementation block (the ~lines 39–389
containing the old createPartnerCommission body) from
apps/web/lib/partners/create-partner-commission.ts so the file only contains the
current active implementation; ensure you delete the entire commented region
(the old earnings/reward/status logic, prisma lookups, commission.create
try/catch and associated logs) while leaving any exports or the current
createPartnerCommission function intact so git history retains the removed code
if needed.
🤖 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/lib/zod/schemas/commissions.ts`:
- Around line 443-444: The Zod schema uses redundant and inconsistent modifiers:
remove the redundant .optional() from the amount field (change amount:
z.number().default(0).optional() to just amount: z.number().default(0)) and make
quantity explicitly optional to match intent (change quantity:
z.number().default(1) to quantity: z.number().default(1).optional()); this
leverages Zod 4 behavior where .default() already makes input optional while
keeping defaults applied.

---

Outside diff comments:
In `@apps/web/lib/actions/partners/create-manual-commission.ts`:
- Around line 250-274: The saleEvent mapping that pushes to commissionsToCreate
is passing the full user object (property user) instead of the expected userId;
change the payload created in the saleEventData.map (the object inside
commissionsToCreate.push) to remove the user property and add userId: user.id so
it matches the other code paths and validation expectations (ensure the key is
exactly userId and not user to keep consistency with the rest of the commission
creation logic).

---

Nitpick comments:
In `@apps/web/app/`(ee)/api/workflows/create-partner-commission/route.ts:
- Around line 315-338: The subscription duration is being recalculated in the
block that checks reward.maxDuration; instead of recomputing differenceInMonths,
use the previously computed value stored on the context
(context.subscriptionDurationMonths) to avoid duplication and drift. Replace the
local subscriptionDurationMonths calculation in the maxDuration branch with a
read of context.subscriptionDurationMonths (and fallback to 0 or a safe default
if undefined), leaving the existing logAndReturn calls, and keep references to
reward, firstCommission, createdAt, partnerId, and event unchanged.
- Around line 501-534: The Promise.allSettled call silences failures from
sendWorkspaceWebhook, sendPartnerPostback, syncTotalCommissions, and
notifyPartnerCommission; after awaiting Promise.allSettled([...]) inspect the
returned results array and log any rejected entries with contextual info
(include the function name, partnerId, programId, workspace id, and the
rejection reason) so individual side-effect failures are visible; use the unique
call sites (sendWorkspaceWebhook, sendPartnerPostback, syncTotalCommissions,
notifyPartnerCommission) to map results to their source when creating the log
message.
- Around line 166-186: The destructured variable named context inside the
stepCreateCommission function is shadowing the Upstash workflow context
parameter; rename the destructured context to rewardContext and update all uses
inside stepCreateCommission (e.g., where context is accessed later in the
function) to rewardContext so the workflow-level context is no longer shadowed
and clarity is preserved; ensure you update every reference (places currently
using context within stepCreateCommission such as the reward handling/creation
code) to the new rewardContext identifier.
- Line 385: The TODO on the conditional spread "(...(createdAt && { createdAt
}))" needs clarification: decide whether createdAt should be accepted from the
request or always set server-side, then update the code and comment accordingly
— if createdAt must be client-controlled, add explicit validation in the
createPartnerCommission handler (validate format/allowed range) and keep the
spread with a clear comment; if it must be server-assigned, remove the spread
and set createdAt when constructing the model (e.g., in the
createPartnerCommission function) and delete the TODO; if you can’t decide now,
replace the TODO with a one-line TODO linked to a freshly opened issue tracking
whether createdAt may be supplied by clients and add a brief test covering both
behaviors.

In `@apps/web/lib/cron/qstash-workflow.ts`:
- Around line 79-111: getWorkflowConfig currently reads properties off body
(typed Record<string, unknown>) unsafely; add type-safe accessors by defining
per-workflow input schemas (e.g., zod schemas or TypeScript runtime validators)
for each workflowType (like "partner-approved" and "create-partner-commission"),
validate/parse body inside getWorkflowConfig (or a helper like
parsePartnerApprovedBody / parseCreatePartnerCommissionBody) and then extract
strongly-typed fields into the correlation object; ensure on validation failure
you handle/throw a clear error or return an empty correlation consistently so
downstream code sees only validated, typed values.

In `@apps/web/lib/partners/create-partner-commission.ts`:
- Around line 39-389: Remove the large commented-out legacy implementation block
(the ~lines 39–389 containing the old createPartnerCommission body) from
apps/web/lib/partners/create-partner-commission.ts so the file only contains the
current active implementation; ensure you delete the entire commented region
(the old earnings/reward/status logic, prisma lookups, commission.create
try/catch and associated logs) while leaving any exports or the current
createPartnerCommission function intact so git history retains the removed code
if needed.
🪄 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: b8ebff2b-2ad2-49d6-ba5e-e9796a647dc5

📥 Commits

Reviewing files that changed from the base of the PR and between 5256a32 and d7b7f1d.

📒 Files selected for processing (23)
  • apps/web/app/(ee)/api/cron/utils.ts
  • apps/web/app/(ee)/api/partner-profile/referrals/route.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts
  • apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts
  • apps/web/app/(ee)/api/workflows/partner-approved/route.ts
  • apps/web/lib/actions/partners/bulk-approve-partners.ts
  • apps/web/lib/actions/partners/create-clawback.ts
  • apps/web/lib/actions/partners/create-manual-commission.ts
  • apps/web/lib/api/conversions/track-lead.ts
  • apps/web/lib/api/conversions/track-sale.ts
  • apps/web/lib/api/partners/applications/approve-partner.ts
  • apps/web/lib/bounty/api/approve-bounty-submission.ts
  • apps/web/lib/cron/index.ts
  • apps/web/lib/cron/qstash-workflow-logger.ts
  • apps/web/lib/cron/qstash-workflow.ts
  • apps/web/lib/integrations/shopify/create-sale.ts
  • apps/web/lib/integrations/shopify/process-order.ts
  • apps/web/lib/partners/create-partner-commission.ts
  • apps/web/lib/types.ts
  • apps/web/lib/zod/schemas/commissions.ts
  • apps/web/lib/zod/schemas/rewards.ts
  • apps/web/scripts/programs/backfill-reuse-commission.ts
💤 Files with no reviewable changes (1)
  • apps/web/lib/cron/qstash-workflow-logger.ts

Comment thread apps/web/lib/zod/schemas/commissions.ts

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts (1)

390-413: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

customer is never returned at top level — fraud detection and postback customer data silently break.

The select nests customer inside the created commission, and the success path returns logAndReturn({ commission, isFirstCommission, ... }) without a top-level customer. But StepCreateCommissionOutput declares customer as a separate field, and the handler at Line 89 destructures it (const { commission, customer, isFirstCommission } = ...) and forwards it to stepRunSideEffects. Since customer is optional in the output type, TypeScript doesn't catch this, so at runtime customer is always undefined.

Downstream effects:

  • detectAndRecordFraudEvent (Line 457) is gated on if (customer && eventId && clickEvent) → never runs.
  • The partner postback payload (Line 543) is built with customer: undefined.
🐛 Proposed fix: split customer out of the created commission
-    const commission = await prisma.commission.create({
+    const { customer, ...commission } = await prisma.commission.create({
       data: {
         ...
       },
       select: {
         id: true,
         earnings: true,
         currency: true,
         customer: {
           select: {
             id: true,
             email: true,
             name: true,
           },
         },
       },
     });

     console.log(prettyPrint(commission));

     const isFirstCommission =
       event !== "custom" ? firstCommission === null : undefined;

     return logAndReturn({
       commission,
+      customer,
       isFirstCommission,
       outputLog: `Created a ${event} commission ${commission.id} (${currencyFormatter(commission.earnings, { currency: commission.currency })}) for ${partnerId}`,
     });
🤖 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/workflows/create-partner-commission/route.ts around
lines 390 - 413, The created DB result currently nests customer inside
commission so the handler never returns a top-level customer; after the create
call (the variable commission), extract the nested customer into its own const
(e.g. const customer = commission.customer) and build a new commission object
without the customer property (or delete commission.customer) and then pass {
commission: newCommission, customer, isFirstCommission, ... } into logAndReturn;
this ensures StepCreateCommissionOutput actually includes a top-level customer
consumed by the caller (and downstream functions like
stepRunSideEffects/detectAndRecordFraudEvent receive a real customer object).
🤖 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)/api/workflows/create-partner-commission/route.ts:
- Line 404: Remove the leftover debug log call
console.log(prettyPrint(commission)) from the create-partner-commission handler
in route.ts; do not replace it with another dump of commission (which may
contain PII), and if you need structured telemetry use the existing sanitized
fields or processLogger with explicitly non-PII data instead.

In `@apps/web/lib/cron/qstash-workflow.ts`:
- Line 31: The QStash workflow trigger is using APP_DOMAIN which is not
reachable from Upstash in dev; update the URL construction in
apps/web/lib/cron/qstash-workflow.ts to use APP_DOMAIN_WITH_NGROK (or a clear
env override) instead of APP_DOMAIN so Upstash can reach local
endpoints—specifically change the url that builds
`${APP_DOMAIN}/api/workflows/${workflow.workflowType}` to use
APP_DOMAIN_WITH_NGROK (or read NEXT_PUBLIC_NGROK_URL when not in production) so
workflow.workflowType endpoints are publicly reachable during local testing.

---

Outside diff comments:
In `@apps/web/app/`(ee)/api/workflows/create-partner-commission/route.ts:
- Around line 390-413: The created DB result currently nests customer inside
commission so the handler never returns a top-level customer; after the create
call (the variable commission), extract the nested customer into its own const
(e.g. const customer = commission.customer) and build a new commission object
without the customer property (or delete commission.customer) and then pass {
commission: newCommission, customer, isFirstCommission, ... } into logAndReturn;
this ensures StepCreateCommissionOutput actually includes a top-level customer
consumed by the caller (and downstream functions like
stepRunSideEffects/detectAndRecordFraudEvent receive a real customer object).
🪄 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: 26c84dc9-aa21-4703-a69f-f2776f054b95

📥 Commits

Reviewing files that changed from the base of the PR and between d7b7f1d and e49d717.

📒 Files selected for processing (3)
  • apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts
  • apps/web/lib/bounty/api/approve-bounty-submission.ts
  • apps/web/lib/cron/qstash-workflow.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/lib/bounty/api/approve-bounty-submission.ts

Comment thread apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts Outdated
Comment thread apps/web/lib/cron/qstash-workflow.ts

@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: 1

🤖 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/tests/utils/verify-commission.ts`:
- Line 14: The inline comment describing the test timeout is stale: update the
comment that currently reads “timeout after 45 seconds” to match the constant
TIMEOUT_MS (60 seconds) in this file (verify-commission.ts); locate the comment
near the verify/timeout assertion and change the text to “timeout after 60
seconds” (or reference TIMEOUT_MS) so the comment and the TIMEOUT_MS constant
stay consistent.
🪄 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: c7d34cbc-636d-4087-8c66-37e6a3c838dd

📥 Commits

Reviewing files that changed from the base of the PR and between 90e7ced and 5f844ba.

📒 Files selected for processing (2)
  • apps/web/tests/utils/verify-commission.ts
  • apps/web/tests/utils/verify-fraud-event.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/tests/utils/verify-fraud-event.ts

Comment thread apps/web/tests/utils/verify-commission.ts
@steven-tey

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review, any breaking changes from existing commission creation behavior?

@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the new workflow route
cat apps/web/app/\(ee\)/api/workflows/create-partner-commission/route.ts

Length of output: 16830


🏁 Script executed:

#!/bin/bash
# Read the queue helper
cat apps/web/lib/partners/queue-partner-commission-creation.ts

Length of output: 1193


🏁 Script executed:

#!/bin/bash
# Read the updated track-lead and track-sale
cat apps/web/lib/api/conversions/track-lead.ts
cat apps/web/lib/api/conversions/track-sale.ts

Length of output: 28565


🏁 Script executed:

#!/bin/bash
# Read the updated qstash-workflow.ts
cat apps/web/lib/cron/qstash-workflow.ts

Length of output: 3020


🏁 Script executed:

#!/bin/bash
# Read the updated approve-bounty-submission
cat apps/web/lib/bounty/api/approve-bounty-submission.ts

Length of output: 5106


🏁 Script executed:

#!/bin/bash
# Read the updated create-clawback and create-manual-commission
cat apps/web/lib/actions/partners/create-clawback.ts
cat apps/web/lib/actions/partners/create-manual-commission.ts

Length of output: 17589


🏁 Script executed:

#!/bin/bash
# Read the createPartnerCommissionSchema
cat apps/web/lib/zod/schemas/commissions.ts

Length of output: 14688


🏁 Script executed:

#!/bin/bash
# Check the checkout-session-completed and invoice-paid webhooks
cat apps/web/app/\(ee\)/api/stripe/integration/webhook/checkout-session-completed.ts
cat apps/web/app/\(ee\)/api/stripe/integration/webhook/invoice-paid.ts

Length of output: 33432


🏁 Script executed:

#!/bin/bash
# Check the Shopify files
cat apps/web/lib/integrations/shopify/create-sale.ts
cat apps/web/lib/integrations/shopify/process-order.ts

Length of output: 12323


🏁 Script executed:

#!/bin/bash
# Check the cron index and utils
cat apps/web/lib/cron/index.ts
cat apps/web/app/\(ee\)/api/cron/utils.ts

Length of output: 809



@steven-tey Here's a thorough review of the PR focused on breaking changes from the existing commission creation behavior.


Summary

The core architectural change is converting commission creation from synchronous and in-process (createPartnerCommission) to asynchronous via QStash workflow (queuePartnerCommissionCreationPOST /api/workflows/create-partner-commission). Most of the logic is preserved correctly inside the new workflow route. However, there are several behavioral regressions and bugs introduced in the migration.


🔴 Breaking Changes / Bugs

1. status: "refunded" silently dropped for manual commission import

File: apps/web/lib/actions/partners/create-manual-commission.ts

In the useExistingEvents (Stripe invoice import) path, the commission payload still includes status: "refunded" for refunded invoices:

commissionsToCreate.push(
  ...saleEventData.map((saleEvent) => ({
    // ...
    ...(stripeCustomerInvoices.find(
      (invoice) => invoice.id === saleEvent.invoice_id,
    )?.refunded && {
      status: "refunded",  // ← NOT in createPartnerCommissionSchema
    }),
    user,  // ← full user object, NOT in createPartnerCommissionSchema (needs userId: user.id)
    // ...
  })),
);

createPartnerCommissionSchema has no status field and no user field. The initialPayloadParser in the workflow route calls createPartnerCommissionSchema.parse(...), which strips both fields by default. Result: refunded invoices will create commissions with status: "pending" instead of "refunded", and userId will be undefined for all commissions created via this path.


2. Stale totalCommissions in sale.created / lead.created webhooks

File: apps/web/lib/partners/queue-partner-commission-creation.ts

return {
  partner,
  links,
  programEnrollment,
  webhookPartner: constructWebhookPartner(result),  // ← no earnings adjustment
};

The webhookPartner returned here is built before the commission exists, so totalCommissions reflects the pre-commission value. Contrast with stepRunSideEffects in the workflow, which correctly does:

const webhookPartner = constructWebhookPartner(programEnrollment, {
  totalCommissions:
    toCentsNumber(programEnrollment.totalCommissions) + commission.earnings,
});

Every caller (track-sale, track-lead, Stripe webhooks, Shopify) uses result?.webhookPartner for the sale.created / lead.created workspace webhook payload. Downstream integrations that read partner.totalCommissions from these webhooks will receive a value that's too low.


3. Fraud detection silently skipped for Shopify discount code leads and Stripe promo code leads

Files: apps/web/lib/integrations/shopify/process-order.tsattributeViaDiscountCode, apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.tsattributeViaPromoCode

Neither path passes clickEvent to queuePartnerCommissionCreation:

// process-order.ts attributeViaDiscountCode
result = await queuePartnerCommissionCreation({
  event: "lead",
  programId: link.programId,
  // ...
  // ← no clickEvent
});

// checkout-session-completed.ts attributeViaPromoCode
result = await queuePartnerCommissionCreation({
  event: "lead",
  // ...
  // ← no clickEvent
});

In the workflow's stepRunSideEffects, fraud detection requires commission.customer && eventId && clickEvent. Without clickEvent, fraud detection is entirely skipped for these two attribution paths. The old createPartnerCommission would have run it via the detectAndRecordFraudEvent call in those same paths.


4. bountySubmission.commissionId not set synchronously

File: apps/web/lib/bounty/api/approve-bounty-submission.ts

Previously, the commission was created synchronously and commissionId was set on the bounty submission in the same DB update. Now:

await queuePartnerCommissionCreation({
  event: "custom",
  // ...
  bountySubmissionId: submissionId,
});

The workflow's set-bounty-commission step links the commission later via updateMany (with commissionId: null guard). There is a window — potentially minutes — where bountySubmission.status = "approved" but bountySubmission.commissionId = null. Any logic or UI that reads commissionId immediately after approval will see null.


5. Duplicate commission risk on workflow retry for events without unique DB constraints

File: apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts

} catch (error) {
  if (error.code !== "P2002") {
    await log({ message: outputLog, type: "errors", mention: true });
    throw new WorkflowRetryAfterError(error.message, "5s");  // ← will retry
  }
  return logAndReturn({ commission: null, outputLog });
}

The workflow retries up to 5 times (QStash retries: 5) on any non-P2002 error. If a commission was successfully persisted but an error occurred before the workflow step returned (e.g., a network timeout after the prisma.commission.create call), the retry will attempt to create the commission again. For click events there is no invoiceId/eventId unique DB constraint to catch this — a duplicate commission could be created silently. Consider wrapping the prisma.commission.create in an idempotency check (e.g., use upsert on eventId when present, or store the workflow run ID).


🟡 Behavioral Changes Worth Noting (Non-Breaking but Observable)

6. executeWorkflows (partnerMetricsUpdated) called twice per sale/lead

Callsites such as track-sale.ts, track-lead.ts, checkout-session-completed.ts, and invoice-paid.ts call executeWorkflows for partnerMetricsUpdated immediately after queuePartnerCommissionCreation. The workflow's stepRunSideEffects also calls executeDubWorkflows for partnerMetricsUpdated (under shouldTriggerWorkflow). Unless skipWorkflow: true is passed, this triggers the metric-updated workflow twice per event.

7. Commission creation is now entirely async

API consumers that relied on the commission being present in the database by the time the /track/sale, /track/lead, or bounty-approval responses are returned will no longer see it. This applies particularly to integration tests and any polling logic that starts immediately after an API call.

8. Fraud detection is now delayed

Fraud detection previously ran within the same request lifecycle (or in waitUntil). It now runs inside the workflow, which may execute with a delay of seconds to minutes depending on QStash queue depth.


🟢 What's Working Well

  • The commission business logic (deduplication, max-duration checks, fraud detection, isFirstConversion, clawback handling) is faithfully reproduced in the workflow route.
  • The flowControl: { parallelism: 1 } per customerId ?? partnerId correctly serializes concurrent commission creation for the same customer/partner, preventing race conditions on lead deduplication.
  • The WorkflowRetryAfterError pattern correctly avoids Slack noise for expected unique-constraint violations (P2002).
  • The failureFunction with structured logging and Axiom flush is a good improvement over the removed createWorkflowLogger.
  • rewardContextSchema date coercion (z.coerce.date()) is a good fix for serialized date payloads over QStash.

Top Items to Fix Before Merging

# File Issue
1 create-manual-commission.ts Add status to createPartnerCommissionSchema OR handle refunded post-creation; replace user with userId: user.id in useExistingEvents path
2 queue-partner-commission-creation.ts Pass commission earnings to constructWebhookPartner — OR document that sale.created/lead.created webhooks intentionally omit the new commission from totalCommissions
3 process-order.ts, checkout-session-completed.ts Pass clickEvent in promo-code/discount-code lead commission payloads to restore fraud detection
4 Workflow route Add idempotency guard for WorkflowRetryAfterError to prevent duplicate commissions for events without unique constraints
✅ Actions performed

Full review triggered.

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts (1)

698-711: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pass clickEvent to restore fraud detection for promo-code leads.

The clickEvent is available (line 646) but not passed here. The workflow's fraud detection requires clickEvent.url and clickEvent.referer to function. Without it, fraud checks are silently skipped for promo-code attributions—a regression from the prior synchronous behavior.

🐛 Proposed fix
         result = await queuePartnerCommissionCreation({
           event: "lead",
           programId: link.programId,
           partnerId: link.partnerId,
           linkId: link.id,
           eventId: leadEvent.event_id,
           customerId: customer.id,
           quantity: 1,
           context: {
             customer: {
               country: customer.country,
             },
           },
+          clickEvent: {
+            url: leadEvent.url,
+            referer: leadEvent.referer,
+          },
         });
🤖 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/stripe/integration/webhook/checkout-session-completed.ts
around lines 698 - 711, The call to queuePartnerCommissionCreation is missing
the clickEvent payload so promo-code lead fraud checks (which need
clickEvent.url and clickEvent.referer) are skipped; update the invocation of
queuePartnerCommissionCreation (the function call shown in the diff) to include
the existing clickEvent object in the payload—e.g. add clickEvent into the
context (alongside customer) or as the appropriate parameter expected by
queuePartnerCommissionCreation so the workflow receives clickEvent.url and
clickEvent.referer for fraud detection.
apps/web/lib/integrations/shopify/process-order.ts (1)

157-170: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing clickEvent for fraud detection in discount-code attribution.

The attributeViaDiscountCode function has access to clickEvent from recordFakeClick() (line 92) but doesn't pass it to queuePartnerCommissionCreation. This means fraud detection will be skipped for Shopify discount-code leads, unlike the previous synchronous behavior.

To restore fraud detection, pass the clickEvent:

🐛 Proposed fix to pass clickEvent
       result = await queuePartnerCommissionCreation({
         event: "lead",
         programId: link.programId,
         partnerId: link.partnerId,
         linkId: link.id,
         eventId: leadEvent.event_id,
         customerId: customer.id,
         quantity: 1,
         context: {
           customer: {
             country: customer.country,
           },
         },
+        clickEvent: {
+          url: clickEvent.url,
+          referer: clickEvent.referer,
+        },
       });
🤖 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/integrations/shopify/process-order.ts` around lines 157 - 170,
In attributeViaDiscountCode, include the clickEvent captured from
recordFakeClick() when calling queuePartnerCommissionCreation so fraud detection
runs; locate the attributeViaDiscountCode function and the
queuePartnerCommissionCreation call and add the clickEvent (e.g., pass
clickEvent: clickEvent or include it inside the context object alongside
customer) so the queued partner commission creation receives the clickEvent for
fraud checks.
apps/web/lib/actions/partners/create-manual-commission.ts (1)

264-274: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Schema mismatch: status and user fields will be dropped or rejected.

The commission payload includes status: "refunded" (line 267) and the full user object (line 269), but createPartnerCommissionSchema (in commissions.ts) does not accept a status field and expects userId: string instead of user: object.

Impact:

  • Refunded invoices will be queued as pending commissions instead of refunded.
  • The user object will fail schema validation or be silently dropped, leaving userId undefined.
🔧 Proposed fix
           // if the invoice payment was refunded on Stripe, set the commission status to refunded as well
           ...(stripeCustomerInvoices.find(
             (invoice) => invoice.id === saleEvent.invoice_id,
-          )?.refunded && {
-            status: "refunded",
-          }),
-          user,
+          )?.refunded && {}), // TODO: handle refunded status in workflow or post-creation step
+          userId: user.id,
           context: {
             customer: { country: customer.country },
           },

Note: Since the schema doesn't support status, you'll need to either:

  1. Add status to createPartnerCommissionSchema and handle it in the workflow, or
  2. Mark refunded invoices in a post-creation step (e.g., via a separate workflow or commission update).
🤖 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/actions/partners/create-manual-commission.ts` around lines 264 -
274, The commission payload currently includes status and a full user object
which doesn't match createPartnerCommissionSchema (it expects userId: string and
has no status); update the payload in create-manual-commission.ts (the mapping
that builds the commission objects for createPartnerCommissionSchema) to replace
user with userId (e.g., user.id) and remove the status field, and if refunded
state must be recorded either add status to createPartnerCommissionSchema and
propagate it through the workflow or implement a separate post-creation step to
mark commissions as refunded.
apps/web/lib/api/conversions/track-sale.ts (1)

467-499: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Webhook partner data excludes the newly queued commission.

queuePartnerCommissionCreation returns webhookPartner built from the enrollment fetched before the commission is queued (line 472). The sale.created webhook (line 539) will include partner totals that don't reflect this new commission.

Impact:

  • partner.totalCommissions and related aggregates in the webhook payload will be lower than expected.
  • This is a behavioral change from the previous synchronous createPartnerCommission flow, which included the new commission in totals.
💡 Suggested fix

Option 1 (preferred): Pass the earnings/amount to queuePartnerCommissionCreation and have it compute updated totals before returning webhookPartner.

Option 2: Fetch or recompute partner totals after queuing the commission (before sending the webhook).

Option 3: Document that webhook totals may lag and rely on eventual consistency (the workflow will update totals later).

Also applies to: 539-539

🤖 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/api/conversions/track-sale.ts` around lines 467 - 499, The
webhook payload currently uses webhookPartner returned by
queuePartnerCommissionCreation which is based on enrollment data fetched before
queuing the commission, causing partner totals to omit the new commission;
update queuePartnerCommissionCreation (or its call site in track-sale.ts) to
include the commission amount/earnings (e.g., pass saleData.amount or computed
earnings) and have queuePartnerCommissionCreation compute and return updated
totals in webhookPartner before resolving, or alternatively, after awaiting
queuePartnerCommissionCreation, re-fetch/recompute partner totals and replace
webhookPartner fields used by the sale.created webhook so the payload includes
the newly queued commission.
♻️ Duplicate comments (1)
apps/web/tests/utils/verify-commission.ts (1)

46-46: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stale comment: timeout is now 60s, not 45s.

TIMEOUT_MS is 60000, but Line 46 still reads "timeout after 45 seconds."

Suggested patch
-  // Poll for commission every 5 seconds, timeout after 45 seconds
+  // Poll for commission every 5 seconds, timeout after 60 seconds
🤖 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/tests/utils/verify-commission.ts` at line 46, Update the stale
inline comment that says "timeout after 45 seconds" to reflect the actual
TIMEOUT_MS value of 60000 (60 seconds); locate the TIMEOUT_MS constant and the
polling comment near it in verify-commission.ts (the comment on or around the
polling/pollEvery 5000ms logic) and change the text to "timeout after 60
seconds" so the comment matches the code behavior.
🤖 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)/api/workflows/create-partner-commission/route.ts:
- Around line 388-407: The retry path can create duplicate click commissions
because stepCreateCommission calls prisma.commission.create without an
idempotency key and click rows lack a unique constraint; add a stable
idempotency key for clicks (e.g., clickEventId or workflowRunId) to the
Commission model (making it unique for click-type commissions) and change
stepCreateCommission to perform an idempotent write (use
prisma.commission.upsert or insert-on-conflict logic keyed on that new field
instead of prisma.commission.create), keeping existing eventId/invoiceId
behavior, and ensure error handling around WorkflowRetryAfterError/logAndReturn
still inspects the upsert result rather than blindly retrying.
- Around line 513-521: The code passes a possibly undefined group into
notifyPartnerCommission (via programEnrollment.partnerGroup ??
program.groups[0]) which will crash when notifyPartnerCommission reads
group.holdingPeriodDays; before calling notifyPartnerCommission, compute a local
group variable (e.g., const partnerGroup = programEnrollment.partnerGroup ??
program.groups[0]) and guard it: if partnerGroup is undefined then either skip
calling notifyPartnerCommission (and optionally log a warning) or throw a clear
error; otherwise pass partnerGroup into notifyPartnerCommission. Ensure you
update the call site that currently uses the inline fallback so
notifyPartnerCommission always receives a defined group.

In `@apps/web/tests/utils/verify-fraud-event.ts`:
- Line 32: Update the stale inline comment to reflect the actual timeout used:
change "timeout after 45 seconds" to "timeout after 60 seconds" (the constant
TIMEOUT_MS = 60000 governs the timeout) in the verify-fraud-event utility so the
comment matches the code.

---

Outside diff comments:
In
`@apps/web/app/`(ee)/api/stripe/integration/webhook/checkout-session-completed.ts:
- Around line 698-711: The call to queuePartnerCommissionCreation is missing the
clickEvent payload so promo-code lead fraud checks (which need clickEvent.url
and clickEvent.referer) are skipped; update the invocation of
queuePartnerCommissionCreation (the function call shown in the diff) to include
the existing clickEvent object in the payload—e.g. add clickEvent into the
context (alongside customer) or as the appropriate parameter expected by
queuePartnerCommissionCreation so the workflow receives clickEvent.url and
clickEvent.referer for fraud detection.

In `@apps/web/lib/actions/partners/create-manual-commission.ts`:
- Around line 264-274: The commission payload currently includes status and a
full user object which doesn't match createPartnerCommissionSchema (it expects
userId: string and has no status); update the payload in
create-manual-commission.ts (the mapping that builds the commission objects for
createPartnerCommissionSchema) to replace user with userId (e.g., user.id) and
remove the status field, and if refunded state must be recorded either add
status to createPartnerCommissionSchema and propagate it through the workflow or
implement a separate post-creation step to mark commissions as refunded.

In `@apps/web/lib/api/conversions/track-sale.ts`:
- Around line 467-499: The webhook payload currently uses webhookPartner
returned by queuePartnerCommissionCreation which is based on enrollment data
fetched before queuing the commission, causing partner totals to omit the new
commission; update queuePartnerCommissionCreation (or its call site in
track-sale.ts) to include the commission amount/earnings (e.g., pass
saleData.amount or computed earnings) and have queuePartnerCommissionCreation
compute and return updated totals in webhookPartner before resolving, or
alternatively, after awaiting queuePartnerCommissionCreation, re-fetch/recompute
partner totals and replace webhookPartner fields used by the sale.created
webhook so the payload includes the newly queued commission.

In `@apps/web/lib/integrations/shopify/process-order.ts`:
- Around line 157-170: In attributeViaDiscountCode, include the clickEvent
captured from recordFakeClick() when calling queuePartnerCommissionCreation so
fraud detection runs; locate the attributeViaDiscountCode function and the
queuePartnerCommissionCreation call and add the clickEvent (e.g., pass
clickEvent: clickEvent or include it inside the context object alongside
customer) so the queued partner commission creation receives the clickEvent for
fraud checks.

---

Duplicate comments:
In `@apps/web/tests/utils/verify-commission.ts`:
- Line 46: Update the stale inline comment that says "timeout after 45 seconds"
to reflect the actual TIMEOUT_MS value of 60000 (60 seconds); locate the
TIMEOUT_MS constant and the polling comment near it in verify-commission.ts (the
comment on or around the polling/pollEvery 5000ms logic) and change the text to
"timeout after 60 seconds" so the comment matches the code behavior.
🪄 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: a76c9559-0037-48c4-a419-c389dde946f2

📥 Commits

Reviewing files that changed from the base of the PR and between 05158f5 and 0f6236f.

📒 Files selected for processing (27)
  • apps/web/app/(ee)/api/cron/utils.ts
  • apps/web/app/(ee)/api/partner-profile/referrals/route.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts
  • apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts
  • apps/web/app/(ee)/api/workflows/partner-approved/route.ts
  • apps/web/lib/actions/partners/bulk-approve-partners.ts
  • apps/web/lib/actions/partners/create-clawback.ts
  • apps/web/lib/actions/partners/create-manual-commission.ts
  • apps/web/lib/api/conversions/track-lead.ts
  • apps/web/lib/api/conversions/track-sale.ts
  • apps/web/lib/api/partners/applications/approve-partner.ts
  • apps/web/lib/bounty/api/approve-bounty-submission.ts
  • apps/web/lib/cron/index.ts
  • apps/web/lib/cron/qstash-workflow-logger.ts
  • apps/web/lib/cron/qstash-workflow.ts
  • apps/web/lib/integrations/shopify/create-sale.ts
  • apps/web/lib/integrations/shopify/process-order.ts
  • apps/web/lib/partners/create-partner-commission.ts
  • apps/web/lib/partners/queue-partner-commission-creation.ts
  • apps/web/lib/types.ts
  • apps/web/lib/zod/schemas/commissions.ts
  • apps/web/lib/zod/schemas/rewards.ts
  • apps/web/scripts/programs/backfill-reuse-commission.ts
  • apps/web/tests/fraud/index.test.ts
  • apps/web/tests/utils/verify-commission.ts
  • apps/web/tests/utils/verify-fraud-event.ts
💤 Files with no reviewable changes (2)
  • apps/web/lib/cron/qstash-workflow-logger.ts
  • apps/web/lib/partners/create-partner-commission.ts

Comment thread apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts
Comment thread apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts
Comment thread apps/web/tests/utils/verify-fraud-event.ts Outdated
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