Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@devkiran
Copy link
Collaborator

@devkiran devkiran commented Oct 8, 2025

Summary by CodeRabbit

  • Bug Fixes

    • Prevents duplicate commission payouts by atomically short-circuiting already-approved submissions.
    • Ensures eligibility checks run against the latest performance count to avoid stale evaluation.
  • Refactor

    • Consolidated approval and submission updates into a single transactional flow for improved reliability and data integrity.
    • Better error handling to avoid partial updates when commission creation fails.
  • Notifications

    • Continues sending confirmation emails to partners upon successful approval.

@vercel
Copy link
Contributor

vercel bot commented Oct 8, 2025

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

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Oct 8, 2025 2:29pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 8, 2025

Walkthrough

Reworked the bounty-award flow into a single database transaction that finds or creates the partner submission, updates performanceCount, short-circuits if already approved, evaluates trigger, creates and links a partner commission if needed, updates submission to approved, and sends the partner email.

Changes

Cohort / File(s) Summary of Changes
Transactional workflow refactor
apps/web/lib/api/workflows/execute-award-bounty-workflow.ts
Replaced prior upsert flow with an explicit transaction that: selects the submission (row lock), conditionally inserts or updates performanceCount, short-circuits on existing approved status, re-evaluates the trigger against the updated count, creates a partner commission only when none exists, updates the submission with commissionId and approved status, and retains sending the partner email. Removed local extraction of bounty.groups/submissions and simplified checks to use bounty.groups directly.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller as Workflow Invoker
  participant WF as executeAwardBountyWorkflow
  participant DB as Database
  participant CS as Commission Service
  participant ES as Email Service

  Caller->>WF: invoke(bounty, partner, payload)
  WF->>DB: BEGIN TRANSACTION
  WF->>DB: SELECT submission FOR UPDATE (bountyId, partnerId)
  alt submission exists
    WF->>DB: if status != "approved" -> UPDATE performanceCount (+1)
  else no submission
    WF->>DB: INSERT submission (performanceCount = 1)
  end

  WF->>WF: Evaluate trigger using updated performanceCount
  alt trigger not met
    WF->>DB: COMMIT
    WF-->>Caller: return (no-op)
  else trigger met
    WF->>DB: Check submission.commissionId / status
    alt already approved or commission exists
      WF->>DB: COMMIT
      WF-->>Caller: return (already processed)
    else need commission
      WF->>CS: Create partner commission
      alt commission created
        WF->>DB: UPDATE submission { commissionId, status: "approved" }
        WF->>DB: COMMIT
        WF->>ES: Send approval email
        ES-->>WF: Ack
        WF-->>Caller: return (approved)
      else commission creation fails
        WF->>DB: ROLLBACK
        WF-->>Caller: return (error)
      end
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • TWilson023

Poem

I thump, I fetch, I lock the row,
Count the hops where carrots grow.
Commission sewn and status new,
An email hops — approved for you.
Little rabbit seals the flow 🥕🐇

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title accurately reflects the primary change of fixing the race condition that caused duplicate commissions in the bounty workflow and succinctly describes the core update.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch improve-bounty-action

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
apps/web/lib/api/workflows/execute-award-bounty-workflow.ts (2)

147-153: Evaluate condition against the locked, up-to-date count

If you apply the lock-and-refetch, ensure you evaluate with the latest performanceCount:

-const shouldExecute = evaluateWorkflowCondition({
-  condition,
-  attributes: {
-    [condition.attribute]: bountySubmission.performanceCount,
-  },
-});
+const shouldExecute = evaluateWorkflowCondition({
+  condition,
+  attributes: {
+    [condition.attribute]: lockedSubmission.performanceCount,
+  },
+});

107-107: Use Serializable isolation to strengthen correctness under concurrency

Add isolationLevel: Serializable to the interactive transaction to reduce write skew and phantom reads:

// Add at top of file:
import { Prisma } from "@dub/prisma/client";

// Change the transaction invocation:
const updatedSubmission = await prisma.$transaction(
  async (tx) => {
    // ...
  },
  { isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
);

This complements the row-level lock and atomic increment.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dcc5196 and 153441f.

📒 Files selected for processing (1)
  • apps/web/lib/api/workflows/execute-award-bounty-workflow.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/lib/api/workflows/execute-award-bounty-workflow.ts (3)
apps/web/lib/api/create-id.ts (1)
  • createId (66-71)
apps/web/lib/api/workflows/execute-workflows.ts (1)
  • evaluateWorkflowCondition (93-121)
apps/web/lib/partners/create-partner-commission.ts (1)
  • createPartnerCommission (57-368)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (1)
apps/web/lib/api/workflows/execute-award-bounty-workflow.ts (1)

73-74: Group eligibility check looks good

Using bounty.groups.map(groupId) is clear and correct given the include.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
apps/web/lib/api/workflows/execute-award-bounty-workflow.ts (1)

167-175: Commission creation is outside the DB transaction; risk of orphan/duplicate commissions on rollback/failure

If createPartnerCommission succeeds but the subsequent bountySubmission.update fails/rolls back, you’ll orphan a commission. Under retries, you can create duplicates. Make the helper transaction‑aware and pass db: tx so “create commission + link + approve” is atomic.

Apply in this file:

-    const { commission } = await createPartnerCommission({
+    const { commission } = await createPartnerCommission({
       event: "custom",
       partnerId,
       programId: bounty.programId,
       amount: bounty.rewardAmount ?? 0,
       quantity: 1,
       description: `Commission for successfully completed "${bounty.name}" bounty.`,
       skipWorkflow: true,
+      db: tx,
     });

And update the helper (apps/web/lib/partners/create-partner-commission.ts):

// imports
import { prisma } from "@dub/prisma";
import type { Prisma, PrismaClient } from "@dub/prisma";

// signature
export const createPartnerCommission = async (
  props: CreatePartnerCommissionProps & { db?: Prisma.TransactionClient | PrismaClient },
) => {
  const db = props.db ?? prisma;

  // use `db` for all DB writes
  // e.g.,
  // const commission = await db.commission.create({ data: { ... }, include: { customer: true } });

  // keep side effects (webhooks/notifications) out of the tx if possible,
  // or gate them to run after commit from the caller.
};

Optional hardening: add an idempotency guard (e.g., unique key on commission metadata by bountySubmissionId) to prevent duplicate commissions even if retried post‑failure.

🧹 Nitpick comments (1)
apps/web/lib/api/workflows/execute-award-bounty-workflow.ts (1)

107-116: Remove redundant pre-read; rely on the upsert result under lock

findUnique here doesn’t lock and is immediately followed by an upsert that does lock/update. Drop the pre-read and re-check status/commissionId using the bountySubmission returned by upsert.

-    const existingSubmission = await tx.bountySubmission.findUnique({
-      where: {
-        bountyId_partnerId: {
-          bountyId,
-          partnerId,
-        },
-      },
-    });
-
-    if (existingSubmission?.status === "approved") {
-      console.log(
-        `Partner ${partnerId} has already been awarded this bounty (bountyId: ${bounty.id}, submissionId: ${existingSubmission.id}).`,
-      );
-      return;
-    }

Then right after the upsert:

     const bountySubmission = await tx.bountySubmission.upsert({ /* ... */ });

+    if (bountySubmission.status === "approved" || bountySubmission.commissionId) {
+      console.log(
+        `Bounty submission ${bountySubmission.id} already approved or has commission ${bountySubmission.commissionId}.`,
+      );
+      return;
+    }

Also applies to: 117-123

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 251b8e5 and 809e71d.

📒 Files selected for processing (1)
  • apps/web/lib/api/workflows/execute-award-bounty-workflow.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/lib/api/workflows/execute-award-bounty-workflow.ts (3)
apps/web/lib/api/create-id.ts (1)
  • createId (66-71)
apps/web/lib/api/workflows/execute-workflows.ts (1)
  • evaluateWorkflowCondition (93-121)
apps/web/lib/partners/create-partner-commission.ts (1)
  • createPartnerCommission (57-368)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (3)
apps/web/lib/api/workflows/execute-award-bounty-workflow.ts (3)

140-142: Good: atomic increment avoids lost updates

Using { increment: performanceCount } fixes the lost‑update race on performanceCount.


107-107: Optional: enforce Serializable isolation on the transaction
Further reduces concurrency anomalies (e.g., write skew). If your Prisma client version supports it, update the call to:

import { Prisma } from "@prisma/client";

const updatedSubmission = await prisma.$transaction(
  async (tx) => { /* ... */ },
  { isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
);

Confirm your installed Prisma version supports TransactionIsolationLevel.Serializable before applying.


105-106: No change needed for performanceCount Δ handling
performanceCount is sourced from context.current, which producers set as per-event deltas (e.g. leads: 1, saleAmount: amount), so using { increment: performanceCount } is correct.

@devkiran devkiran requested a review from steven-tey October 8, 2025 15:19
@devkiran devkiran closed this Oct 9, 2025
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