Skip to content

Add POST /api/commissions for manual commission creation - #3968

Merged
steven-tey merged 62 commits into
mainfrom
create-commissions-api-v1
Jun 5, 2026
Merged

Add POST /api/commissions for manual commission creation#3968
steven-tey merged 62 commits into
mainfrom
create-commissions-api-v1

Conversation

@devkiran

@devkiran devkiran commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added async API to create commissions (POST /commissions) with validated request/response and queued processing
    • Optional immediate aggregation of due commissions after manual creation
  • Enhancements

    • Better Stripe invoice import/gating for sales, improved validation and clearer error responses
    • UI: unified commission form submission flow and improved submit-state handling
  • Refactor

    • Migrated manual commission creation to a centralized API-driven implementation
  • Tests

    • Added integration tests covering success and error scenarios

@vercel

vercel Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
dub Error Error Jun 5, 2026 7:41pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 1, 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 migrates manual commission creation from a next-safe-action server action to a type-safe REST API. It adds POST /api/commissions with discriminated Zod schemas, implements backend flows for custom/lead/sale (including Stripe invoice import and deduplication), updates the frontend form to call the API, adds OpenAPI wiring, tests, and small helpers.

Changes

Manual Commission Creation API Implementation

Layer / File(s) Summary
Commission request/response schemas
apps/web/lib/zod/schemas/commissions.ts
Replaces flat createCommissionSchema with createManualCommissionBodySchema as a Zod discriminated union supporting custom, lead, and sale payloads; adds createCommissionResponseSchema for 202 responses.
API endpoint and commission creation core
apps/web/app/(ee)/api/commissions/route.ts, apps/web/lib/api/commissions/create-manual-commissions.ts
Implements POST /api/commissions (workspace/plan/role validation, request parsing) and createManualCommissions core handling custom/lead/sale flows, Stripe invoice import/deduplication, Tinybird event recording, commission queuing, and side-effect orchestration (Prisma updates, workflows).
Frontend commission form refactoring
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/create-commission-sheet.tsx
Migrates from next-safe-action to useApiMutation calling /api/commissions; flattens discriminated schema to a single react-hook-form model, introduces importStripeInvoices, gates SWR invoice fetching, and uses isSubmitting for UI state and cache invalidation.
OpenAPI documentation and routing
apps/web/lib/openapi/commissions/create-commission.ts, apps/web/lib/openapi/commissions/index.ts
Adds createCommission OpenAPI operation wired to request/response schemas and registers POST /commissions in the routes.
Supporting utilities and enhancements
apps/web/lib/api/customers/get-customer-stripe-invoices.ts
Adds optional limit parameter (default 100) to control count of Stripe invoices fetched.
Workflow side-effects
apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts
Conditionally triggers triggerAggregateDueCommissionsCronJob(programId) during partner-commission side effects when requested.
Integration tests for POST endpoint
apps/web/tests/commissions/create-commission.test.ts
Adds tests covering queued 202 responses for custom/lead/sale, validation 422 cases, 404 not-found paths, and 409 invoice-duplicate conflict.
Stripe webhook helper adjustment
apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts
Renames helper return field error to resolvePromotionCodeError for missing/no-discount/success branches.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • dubinc/dub#3634: Related changes around Stripe invoice import/selection and refund-aware handling used by commissions UI and invoice filtering.
  • dubinc/dub#3970: Related Stripe invoice.paid promo-code attribution flow that uses the same helper renamed here.
  • dubinc/dub#3961: Overlapping work on partner commission queuing and workflow integration; this PR replaces the prior server action with the new API + workflow calls.

Suggested reviewers

  • steven-tey
  • pepeladeira

Poem

🐰 A commission sheet takes flight,
Types branch out in tidy light,
POSTs queue work while Stripe invoices sing,
Forms switch to REST and tests take wing,
Backend hums, the frontend hops — hooray for spring!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 accurately and concisely summarizes the main change: adding a POST endpoint to /api/commissions for manual commission creation, which is the primary objective of this PR.
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-commissions-api-v1

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 full review, any breaking changes, bugs?

@coderabbitai

coderabbitai Bot commented Jun 3, 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 "=== invoice-paid.ts resolvePromotionCodeError usage ==="
rg -n "resolvePromotionCodeError\|resolvePromotionCode" --type ts -A2 -B2

echo ""
echo "=== create-manual-commissions.ts full ==="
cat apps/web/lib/api/commissions/create-manual-commissions.ts 2>/dev/null

echo ""
echo "=== commissions zod schema full ==="
cat apps/web/lib/zod/schemas/commissions.ts 2>/dev/null

echo ""
echo "=== commissions route.ts ==="
cat apps/web/app/\(ee\)/api/commissions/route.ts 2>/dev/null

echo ""
echo "=== create-commission.test.ts ==="
cat apps/web/tests/commissions/create-commission.test.ts 2>/dev/null

echo ""
echo "=== resolveLinkAndCustomer implementation ==="
fd "resolve-link-and-customer" --type f
cat apps/web/lib/api/commissions/resolve-link-and-customer.ts 2>/dev/null

echo ""
echo "=== triggerAggregateDueCommissionsCronJob usage context ==="
rg -n "triggerAggregateDueCommissions" --type ts -A3 -B3

echo ""
echo "=== waitUntil usage in create-manual-commissions ==="
rg -n "waitUntil" apps/web/lib/api/commissions/create-manual-commissions.ts -A2 -B2

echo ""
echo "=== isFirstConversion import/usage ==="
rg -n "isFirstConversion" --type ts -A2 -B2

echo ""
echo "=== Check for any remaining old server action imports ==="
rg -rn "create-manual-commission" -g "*.ts" -g "*.tsx"

echo ""
echo "=== createCommissionBodySchema usage in UI ==="
rg -n "createCommissionBodySchema\|createManualCommissionBodySchema" --type ts -A2 -B2

echo ""
echo "=== Check programId derivation in POST route ==="
rg -n "programId" apps/web/app/\(ee\)/api/commissions/route.ts -A2 -B2

echo ""
echo "=== Check for null/undefined safety in resolveLinkAndCustomer ==="
cat apps/web/lib/api/commissions/resolve-link-and-customer.ts 2>/dev/null | head -150

Length of output: 72939



@steven-tey, here's the full review for PR #3968.


Breaking Changes

Change Breaking? Notes
createManualCommissionAction removed ✅ No Internal Next.js server action; no remaining callers
createCommissionSchema removed ✅ No No remaining importers
POST /api/commissions returns 202 async ✅ No Net-new endpoint; not replacing an existing public API
Discriminated union schema replaces flat schema ✅ No Internal rename; no leftover usages
getCustomerStripeInvoices gets optional limit ✅ No Defaults to 100; all callers unaffected

No public breaking changes.


Issues Found

🔴 Debug timing calls left in production route

apps/web/app/(ee)/api/commissions/route.ts has console.time/console.timeEnd calls that should be removed before merging:

// Remove these before merging
console.time("createManualCommissions");
await createManualCommissions({ ... });
console.timeEnd("createManualCommissions");

🟡 saleAmount allows $0 — should be min(1)

In apps/web/lib/zod/schemas/commissions.ts, the sale variant uses .min(0):

saleAmount: centsSchema
  .pipe(z.number().min(0))  // ← allows $0 sale
  .nullish()

custom.amount correctly requires .min(1). A $0 manual sale commission is almost certainly unintentional. Suggest .min(1) for consistency.


🟡 Empty string recorded as invoice_id in Tinybird when no invoiceId is provided

In recordEvents (inside create-manual-commissions.ts), when importStripeInvoices: false and no invoiceId is passed:

saleEventSchemaTBWithTimestamp.parse({
  ...clickEvent,
  event_id: nanoid(16),
  invoice_id: invoiceId ?? "",  // ← empty string recorded instead of null/undefined
  ...
})

This records "" as the invoice_id in Tinybird. If downstream analytics or deduplication logic does any truthiness checks on this field, an empty string may behave unexpectedly. Consider using undefined or null instead.


🟡 Hardcoded limit: 60 in recordEvents inconsistent with the configurable default

getCustomerStripeInvoices was updated in this PR to accept an optional limit (defaulting to 100), but the call in recordEvents still hardcodes 60:

stripeCustomerInvoices = await getCustomerStripeInvoices({
  stripeCustomerId: targetCustomer.stripeCustomerId!,
  stripeConnectId: workspace.stripeConnectId!,
  programId,
  limit: 60,  // ← inconsistent with the new default of 100
});

This seems unintentional given the refactor. If 60 is deliberate (e.g., to cap manual imports), it should be a named constant or documented.


🟡 Custom commission test uses setTimeout(2000) — flake risk

The create custom commission test uses a raw setTimeout instead of the verifyCommission helper already used for lead/sale. This is flake-prone if the queue is slow:

// Fragile:
await new Promise((resolve) => setTimeout(resolve, 2000));

// Prefer the verifyCommission pattern used for lead/sale

✅ Previously Flagged Issues Now Resolved

Issue Status
superRefine to enforce saleAmount when importStripeInvoices: false ✅ Already implemented in the schema
Test case for sale without saleAmount expecting 422 ✅ Already added to validationCases

Positive Highlights

  • Clean discriminated union with superRefine cross-field validation — well done.
  • Conflict detection (409 on duplicate invoiceId) and link ownership (404) are covered in tests.
  • triggerAggregateDueCommissions gated on the last commission in the batch — correct, fires once per batch.
  • waitUntil used correctly for side-effects (Prisma stat updates, workflow execution) so the 202 response isn't blocked.
  • isFirstConversion is correctly evaluated synchronously against the just-upserted customer, which will have sales: 0 on creation.

🧠 Learnings used
Learnt from: steven-tey
Repo: dubinc/dub PR: 0
File: :0-0
Timestamp: 2026-06-03T20:04:17.265Z
Learning: In dubinc/dub PR `#3824`, replacing `context.sale.productId` with `context.sale.products` (array) in `checkout-session-completed.ts` and `invoice-paid.ts` is NOT a breaking change for existing reward conditions. The updated `create-partner-commission/route.ts` workflow mediates this: when `context.sale.products` is present and a `sale.productId` modifier is detected, the workflow iterates through each product, reconstructs `productId` from each product entry, and calls `determinePartnerReward` with a per-product context. So `evaluate-reward-conditions.ts` still receives `productId` correctly — it just comes from the `products` array iteration rather than a top-level `context.sale.productId`. Do not flag this as a breaking change in future reviews.

Learnt from: pepeladeira
Repo: dubinc/dub PR: 3803
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/use-commissions-analytics-query.ts:13-18
Timestamp: 2026-04-24T22:23:11.319Z
Learning: In `apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/use-commissions-analytics-query.ts`, the `status` useMemo intentionally defaults to `"paid"` when `searchParamsObj.commissionStatus` is absent or unrecognized. The Commissions analytics page is designed to show paid commissions by default on first load, not "All". Do not flag this fallback as a bug in future reviews.

Learnt from: devkiran
Repo: dubinc/dub PR: 3919
File: apps/web/lib/sandbox/mock-payout-completion.ts:139-165
Timestamp: 2026-05-22T06:06:02.038Z
Learning: In `apps/web/lib/sandbox/mock-payout-completion.ts`, the grouping logic that reduces multiple payouts per partner to one entry (`partnerPayouts`) and then uses only `partnerPayoutList[0]` when constructing the `sendBatchEmail` payload is intentional. The design deliberately sends at most one email per partner, even when that partner has multiple payouts in the same invoice. Do not flag this as a data-loss issue in future reviews.

Learnt from: steven-tey
Repo: dubinc/dub PR: 3961
File: apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts:388-407
Timestamp: 2026-05-31T20:06:32.526Z
Learning: In dubinc/dub, click commissions are NOT created through the `create-partner-commission` QStash workflow (`POST /api/workflows/create-partner-commission`). Click commissions are handled exclusively by the `/api/cron/aggregate-clicks` cron job. The `create-partner-commission` workflow is only invoked for `lead`, `sale`, and `custom` events.

Learnt from: devkiran
Repo: dubinc/dub PR: 3674
File: apps/web/ui/partners/fraud-risks/commissions-on-hold-table.tsx:44-44
Timestamp: 2026-04-01T10:28:28.392Z
Learning: In the `dubinc/dub` codebase, `fraudEventGroupId` is intentionally excluded from `getCommissionsQuerySchema` (in `apps/web/lib/zod/schemas/commissions.ts`) to avoid exposing it in public OpenAPI specs and SDKs. It is instead extracted directly from `searchParams` in the EE-only API routes (`apps/web/app/(ee)/api/commissions/route.ts` and `apps/web/app/(ee)/api/commissions/count/route.ts`) and forwarded to the underlying query functions (`getCommissions`, `getCommissionsCount`). The filter works correctly at runtime despite being absent from the public schema.

@steven-tey

Copy link
Copy Markdown
Collaborator

@coderabbitai final review

@coderabbitai

coderabbitai Bot commented Jun 4, 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 "=== 1. Check for console.time/timeEnd in route.ts ==="
rg -n "console\.time" apps/web/app/\(ee\)/api/commissions/route.ts

echo ""
echo "=== 2. Full commissions route.ts ==="
cat apps/web/app/\(ee\)/api/commissions/route.ts

echo ""
echo "=== 3. saleAmount schema ==="
rg -n "saleAmount" apps/web/lib/zod/schemas/commissions.ts -A3 -B1

echo ""
echo "=== 4. invoice_id empty string in create-manual-commissions ==="
rg -n "invoice_id" apps/web/lib/api/commissions/create-manual-commissions.ts -A2 -B2

echo ""
echo "=== 5. limit: 60 in create-manual-commissions ==="
rg -n "limit" apps/web/lib/api/commissions/create-manual-commissions.ts -A2 -B2

echo ""
echo "=== 6. setTimeout in test ==="
rg -n "setTimeout" apps/web/tests/commissions/create-commission.test.ts -A2 -B2

echo ""
echo "=== 7. Full test file ==="
cat apps/web/tests/commissions/create-commission.test.ts

echo ""
echo "=== 8. Full zod schemas commissions.ts ==="
cat apps/web/lib/zod/schemas/commissions.ts

echo ""
echo "=== 9. Full create-manual-commissions.ts ==="
cat apps/web/lib/api/commissions/create-manual-commissions.ts

echo ""
echo "=== 10. resolve-link-and-customer full ==="
cat apps/web/lib/api/commissions/resolve-link-and-customer.ts

echo ""
echo "=== 11. Check create-commission openapi ==="
cat apps/web/lib/openapi/commissions/create-commission.ts

echo ""
echo "=== 12. Check for any remaining server action usages ==="
rg -rn "createManualCommissionAction\|createCommissionSchema\|useExistingEvents" --type ts -g "*.ts" -g "*.tsx"

echo ""
echo "=== 13. Check triggerAggregateDueCommissions in workflow route ==="
rg -n "triggerAggregateDueCommissions" apps/web/app/\(ee\)/api/workflows/create-partner-commission/route.ts -A5 -B5

echo ""
echo "=== 14. Check for any null-safety issues in resolveLinkAndCustomer ==="
cat apps/web/lib/api/commissions/resolve-link-and-customer.ts 2>/dev/null

echo ""
echo "=== 15. Check invoice-paid.ts resolvePromotionCodeError fix ==="
rg -n "resolvePromotionCodeError\|resolvePromotionCode" apps/web/app/\(ee\)/api/stripe/integration/webhook/invoice-paid.ts -A3 -B3

echo ""
echo "=== 16. Check createCommissionSheet for any issues ==="
cat "apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/create-commission-sheet.tsx" 2>/dev/null | head -200

echo ""
echo "=== 17. Check for any error handling gaps in POST handler ==="
rg -n "programId\|program" apps/web/app/\(ee\)/api/commissions/route.ts -A3 -B1

echo ""
echo "=== 18. Check requiredPlan in route ==="
rg -n "requiredPlan\|requiredRole" apps/web/app/\(ee\)/api/commissions/route.ts -A2 -B1

Length of output: 60843



@steven-tey, here's the final review summary for PR #3968.


Breaking Changes

None. All previously confirmed: createManualCommissionAction, createCommissionSchema, and useExistingEvents are fully removed with no remaining callers.


Open Issues (from previous reviews + one new critical finding)

🔴 console.time/console.timeEnd still in production route (Lines 92–101, route.ts)

These debug timing calls were flagged in the previous review and are still present:

console.time("createManualCommissions");
await createManualCommissions({ ... });
console.timeEnd("createManualCommissions");

Please remove before merging.


🔴 Empty-string invoiceId causes unique-constraint collisions (Line 510, create-manual-commissions.ts)

This is the most critical unresolved issue. When importStripeInvoices: false and no invoiceId is provided, the code records:

invoice_id: invoiceId ?? "",  // ← empty string, not null

This "" propagates through the entire pipeline:

  1. Tinybird — sale event gets invoice_id: ""
  2. saleEvents.map() — returns invoiceId: ""
  3. commissionsToCreateinvoiceId: saleEvent.invoiceId = ""
  4. queuePartnerCommissionCreation — passes invoiceId: "" to the workflow
  5. Database — if a unique constraint on (invoiceId, programId) is enforced, the second manual sale commission without an invoiceId in the same program will fail with a constraint violation

Fix: use null or undefined instead:

invoice_id: invoiceId || undefined,  // or invoiceId ?? undefined

🟡 saleAmount allows $0 (Line 522, commissions.ts)

Still .min(0):

saleAmount: centsSchema.pipe(z.number().min(0))

custom.amount correctly uses .min(1). A $0 sale commission is almost certainly unintentional — suggest .min(1).


🟡 setTimeout(2000) flake risk in test (Line 80, create-commission.test.ts)

The custom commission test still uses a raw delay. The verifyCommission helper (used for lead/sale) is a more robust pattern. Low priority but worth addressing before CI flakes appear.


Resolved Since Last Review ✅

Issue Status
Hardcoded limit: 60 in getCustomerStripeInvoices call ✅ Fixed — now uses the default 100
superRefine to enforce saleAmount when importStripeInvoices: false ✅ Present (Line 549)
Test case for sale without saleAmount expecting 422 ✅ Present in validationCases

The empty-string invoiceId propagation is the blocker worth fixing before this ships. Everything else is cleanup.


🧠 Learnings used
Learnt from: steven-tey
Repo: dubinc/dub PR: 0
File: :0-0
Timestamp: 2026-06-03T20:04:17.294Z
Learning: In dubinc/dub PR `#3824`, replacing `context.sale.productId` with `context.sale.products` (array) in `checkout-session-completed.ts` and `invoice-paid.ts` is NOT a breaking change for existing reward conditions. The updated `create-partner-commission/route.ts` workflow mediates this: when `context.sale.products` is present and a `sale.productId` modifier is detected, the workflow iterates through each product, reconstructs `productId` from each product entry, and calls `determinePartnerReward` with a per-product context. So `evaluate-reward-conditions.ts` still receives `productId` correctly — it just comes from the `products` array iteration rather than a top-level `context.sale.productId`. Do not flag this as a breaking change in future reviews.

Learnt from: pepeladeira
Repo: dubinc/dub PR: 3803
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/use-commissions-analytics-query.ts:13-18
Timestamp: 2026-04-24T22:23:11.319Z
Learning: In `apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/use-commissions-analytics-query.ts`, the `status` useMemo intentionally defaults to `"paid"` when `searchParamsObj.commissionStatus` is absent or unrecognized. The Commissions analytics page is designed to show paid commissions by default on first load, not "All". Do not flag this fallback as a bug in future reviews.

Learnt from: devkiran
Repo: dubinc/dub PR: 3674
File: apps/web/ui/partners/fraud-risks/commissions-on-hold-table.tsx:44-44
Timestamp: 2026-04-01T10:28:28.392Z
Learning: In the `dubinc/dub` codebase, `fraudEventGroupId` is intentionally excluded from `getCommissionsQuerySchema` (in `apps/web/lib/zod/schemas/commissions.ts`) to avoid exposing it in public OpenAPI specs and SDKs. It is instead extracted directly from `searchParams` in the EE-only API routes (`apps/web/app/(ee)/api/commissions/route.ts` and `apps/web/app/(ee)/api/commissions/count/route.ts`) and forwarded to the underlying query functions (`getCommissions`, `getCommissionsCount`). The filter works correctly at runtime despite being absent from the public schema.

Learnt from: devkiran
Repo: dubinc/dub PR: 3919
File: apps/web/lib/sandbox/mock-payout-completion.ts:139-165
Timestamp: 2026-05-22T06:06:02.038Z
Learning: In `apps/web/lib/sandbox/mock-payout-completion.ts`, the grouping logic that reduces multiple payouts per partner to one entry (`partnerPayouts`) and then uses only `partnerPayoutList[0]` when constructing the `sendBatchEmail` payload is intentional. The design deliberately sends at most one email per partner, even when that partner has multiple payouts in the same invoice. Do not flag this as a data-loss issue in future reviews.

Learnt from: steven-tey
Repo: dubinc/dub PR: 3732
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx:262-305
Timestamp: 2026-04-10T03:43:25.907Z
Learning: In `apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx`, the `disabledTooltip` for the "Confirm selected" button intentionally omits edge-case ineligibility reasons (already-paid/failed status, `mode == null`) for brevity. The tooltip only surfaces the three most common/actionable reasons: below minimum payout amount, partner has not connected payouts, and fraud hold. Do not flag this omission in future reviews.

Learnt from: devkiran
Repo: dubinc/dub PR: 3824
File: apps/web/lib/partners/create-partner-commission.ts:193-195
Timestamp: 2026-04-30T05:05:45.091Z
Learning: In `apps/web/lib/partners/create-partner-commission.ts`, there can only be one reward per partner + program pair. When iterating over multiple products (Stripe line items) to build the `rewards` array, `determinePartnerReward` will always resolve to the same reward object for all products. Therefore, using `rewards[0].reward` as the canonical reward is safe and not order-dependent. Eligibility checks such as `firstCommission.rewardId` and `maxDuration` are safe to run against `rewards[0].reward`.

Learnt from: steven-tey
Repo: dubinc/dub PR: 3961
File: apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts:388-407
Timestamp: 2026-05-31T20:06:35.505Z
Learning: In dubinc/dub, click commissions are NOT created through the `create-partner-commission` QStash workflow (`POST /api/workflows/create-partner-commission`). Click commissions are handled exclusively by the `/api/cron/aggregate-clicks` cron job. The `create-partner-commission` workflow is only invoked for `lead`, `sale`, and `custom` events.

Learnt from: pepeladeira
Repo: dubinc/dub PR: 3923
File: apps/web/lib/actions/partners/update-reward.ts:160-168
Timestamp: 2026-05-18T22:18:20.575Z
Learning: In dubinc/dub, `updateRewardAction` (apps/web/lib/actions/partners/update-reward.ts) only updates the `Reward` row and does NOT touch `programEnrollment` rows or queue any enrollment sync cron job. The enrollment sync cron at `/api/cron/rewards/sync-enrollments` only handles `create` and `delete` actions — there is no update-action cron path. Therefore, calling `notifyPartnersRewardChanged` directly inside `waitUntil(Promise.allSettled([...]))` within `updateRewardAction` is the correct pattern. Do NOT flag this as "premature notification before enrollment sync finalization" or as a duplication risk.

Learnt from: pepeladeira
Repo: dubinc/dub PR: 3923
File: apps/web/lib/actions/partners/update-reward.ts:160-168
Timestamp: 2026-05-18T22:18:20.575Z
Learning: In dubinc/dub, `updateRewardAction` (apps/web/lib/actions/partners/update-reward.ts) only updates the `Reward` row and does NOT write to `programEnrollment` rows. The enrollment sync cron (`/api/cron/rewards/sync-enrollments`) only handles `create` and `delete` actions — there is no "update" enrollment sync. Therefore, calling `notifyPartnersRewardChanged` directly inside `waitUntil(Promise.allSettled([...]))` within `updateRewardAction` is the correct and complete pattern for reward-update partner notifications. Do NOT flag this as premature notification or potential duplication.

Learnt from: devkiran
Repo: dubinc/dub PR: 3676
File: apps/web/lib/api/partner-profile/get-earnings-for-partner.ts:58-59
Timestamp: 2026-04-07T12:03:57.245Z
Learning: In `apps/web/lib/api/partner-profile/get-earnings-for-partner.ts`, the potential conflict between the `linkId` (user-provided filter) and `linkIds` (scope constraint) in the Prisma `where` clause is intentionally handled at the route level (`apps/web/app/(ee)/api/partner-profile/programs/[programId]/earnings/route.ts`): before calling `getEarningsForPartner`, the route explicitly validates that if `parsedQuery.linkId` is present and `assignedLinkIds` is defined, the `linkId` must be in `assignedLinkIds`, throwing a `forbidden` DubApiError otherwise. Therefore, inside `getEarningsForPartner`, when both `linkId` and `linkIds` are present, `linkId` is already guaranteed to be within `linkIds`, making the spread `...(linkIds ? { linkId: { in: linkIds } } : {})` safe and not requiring intersection logic inside the function itself.

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.

3 participants