-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Fix duplicate payout fraud event #3167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
β¦uping and deduplication.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughReplaces partnerId-based grouping with a flexible groupingKey across fraud event lifecycle: removes special-case branch for "partnerDuplicatePayoutMethod", generates a nanoid groupingKey in webhook handling, and updates create/detect/resolve logic and group-key hashing to use groupingKey. Changes
Sequence Diagram(s)sequenceDiagram
participant Webhook as Stripe Webhook
participant Handler as account-updated.ts
participant FraudSvc as create-fraud-events.ts
participant Utils as fraud/utils.ts
participant DB as Database
Webhook->>Handler: receive account.updated event
Handler->>Handler: detect duplicate payout method -> nanoid(10) -> groupingKey
Handler->>FraudSvc: createFraudEvents([{ programId, partnerId, type, groupingKey }])
FraudSvc->>Utils: createFraudEventGroupKey({ programId, type, groupingKey })
Utils-->>FraudSvc: groupKey (hashed)
FraudSvc->>DB: insert fraud events with groupKey, groupingKey, id
DB-->>FraudSvc: insert result
FraudSvc-->>Handler: created event payloads returned
Estimated code review effortπ― 3 (Moderate) | β±οΈ ~25 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touchesβ Failed checks (1 warning)
β Passed checks (2 passed)
β¨ Finishing touches
π§ͺ Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
π§Ή Nitpick comments (1)
apps/web/lib/api/fraud/utils.ts (1)
37-42: Outdated comment referencespartnerIdinstead ofartifactKey.The comment on lines 38-40 mentions "partnerId" but the interface field is now
artifactKey. This should be updated for consistency./** * The batch ID used to group fraud events. This is used when resolving fraud events * to break grouping so resolved events are no longer grouped with pending events - * that share the same programId, partnerId, and type. + * that share the same programId, artifactKey, and type. */ batchId?: string;
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π Files selected for processing (6)
apps/web/app/(ee)/api/fraud/events/raw/route.ts(0 hunks)apps/web/app/(ee)/api/stripe/connect/webhook/account-updated.ts(2 hunks)apps/web/lib/api/fraud/create-fraud-events.ts(1 hunks)apps/web/lib/api/fraud/detect-record-fraud-event.ts(1 hunks)apps/web/lib/api/fraud/resolve-fraud-events.ts(1 hunks)apps/web/lib/api/fraud/utils.ts(1 hunks)
π€ Files with no reviewable changes (1)
- apps/web/app/(ee)/api/fraud/events/raw/route.ts
π§° Additional context used
π§ Learnings (5)
π Common learnings
Learnt from: devkiran
Repo: dubinc/dub PR: 2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.693Z
Learning: In the Dub codebase, multiple payout records can now share the same stripeTransferId because payouts are grouped by partner and processed as single Stripe transfers. This is why the unique constraint was removed from the stripeTransferId field in the Payout model - a single transfer can include multiple payouts for the same partner.
π Learning: 2025-11-24T09:10:12.536Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3089
File: apps/web/lib/api/fraud/fraud-rules-registry.ts:17-25
Timestamp: 2025-11-24T09:10:12.536Z
Learning: In apps/web/lib/api/fraud/fraud-rules-registry.ts, the fraud rules `partnerCrossProgramBan` and `partnerDuplicatePayoutMethod` intentionally have stub implementations that return `{ triggered: false }` because they are partner-scoped rules handled separately during partner application/onboarding flows (e.g., in detect-record-fraud-application.ts), rather than being evaluated per conversion event like other rules in the registry. The stubs exist only to satisfy the `Record<FraudRuleType, ...>` type constraint.
Applied to files:
apps/web/app/(ee)/api/stripe/connect/webhook/account-updated.tsapps/web/lib/api/fraud/detect-record-fraud-event.tsapps/web/lib/api/fraud/utils.tsapps/web/lib/api/fraud/create-fraud-events.tsapps/web/lib/api/fraud/resolve-fraud-events.ts
π Learning: 2025-07-11T16:28:55.693Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.693Z
Learning: In the Dub codebase, multiple payout records can now share the same stripeTransferId because payouts are grouped by partner and processed as single Stripe transfers. This is why the unique constraint was removed from the stripeTransferId field in the Payout model - a single transfer can include multiple payouts for the same partner.
Applied to files:
apps/web/app/(ee)/api/stripe/connect/webhook/account-updated.ts
π Learning: 2025-07-17T06:41:45.620Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2637
File: apps/web/app/(ee)/api/singular/webhook/route.ts:0-0
Timestamp: 2025-07-17T06:41:45.620Z
Learning: In the Singular integration (apps/web/app/(ee)/api/singular/webhook/route.ts), the event names in the singularToDubEvent object have intentionally different casing: "Copy GAID" and "copy IDFA". This casing difference is valid and should not be changed, as these are the correct event names expected from Singular.
Applied to files:
apps/web/lib/api/fraud/detect-record-fraud-event.ts
π Learning: 2025-11-24T08:55:31.332Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3089
File: apps/web/app/(ee)/api/fraud-rules/route.ts:71-87
Timestamp: 2025-11-24T08:55:31.332Z
Learning: In apps/web/app/(ee)/api/fraud-rules/route.ts, fraud rules cannot be created in a disabled state. When using prisma.fraudRule.upsert, the create branch intentionally omits the disabledAt field (defaulting to null, meaning enabled), while the update branch allows toggling enabled/disabled state via the disabledAt field. This is a business logic constraint.
Applied to files:
apps/web/lib/api/fraud/create-fraud-events.ts
𧬠Code graph analysis (2)
apps/web/app/(ee)/api/stripe/connect/webhook/account-updated.ts (1)
apps/web/lib/api/fraud/create-fraud-events.ts (1)
createFraudEvents(13-39)
apps/web/lib/api/fraud/create-fraud-events.ts (3)
packages/prisma/client.ts (1)
FraudRuleType(17-17)packages/prisma/index.ts (1)
prisma(3-9)apps/web/lib/api/fraud/utils.ts (1)
createFraudEventGroupKey(54-60)
β° 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 (7)
apps/web/lib/api/fraud/utils.ts (2)
29-31: LGTM!The hash function is well-implemented using SHA-256 with base64url encoding. The 24-character truncation (144 bits of entropy) provides sufficient uniqueness for group key generation while keeping the keys reasonably short.
54-59: LGTM!The grouping logic correctly builds parts from
programId,type,artifactKey, and optionalbatchId, filtering out undefined values and normalizing to lowercase before hashing. This ensures consistent and deterministic group key generation.apps/web/lib/api/fraud/resolve-fraud-events.ts (1)
50-55: Verify artifactKey consistency for non-partnerId-based fraud events.For fraud events like
partnerDuplicatePayoutMethodthat use a sharedartifactKey(nanoid) across multiple partners during creation, resolving them usingpartnerIdas theartifactKeywill produce a different groupKey pattern. Since the resolution already generates a newbatchIdto break grouping, this may be acceptable, but verify this is the intended behavior.If the original
artifactKeyneeds to be preserved for resolved events, consider storing it in theFraudEventmodel.apps/web/app/(ee)/api/stripe/connect/webhook/account-updated.ts (1)
108-117: LGTM - Key fix for duplicate payout fraud grouping.Generating a shared
artifactKeyviananoid(10)ensures all fraud events for partners with duplicate payout methods are grouped together under the same key. This correctly addresses the issue where previously each partner's fraud events would be grouped separately (bypartnerId), making it difficult to resolve them as a cohesive set.apps/web/lib/api/fraud/detect-record-fraud-event.ts (1)
102-106: LGTM!For conversion-based fraud detection, using
partner.idas theartifactKeyis correct. This maintains the existing partner-specific grouping behavior while aligning with the updated interface that now acceptsartifactKeyinstead ofpartnerId.apps/web/lib/api/fraud/create-fraud-events.ts (2)
6-11: LGTM!The interface design is clean with an optional
artifactKeyand clear inline documentation explaining thepartnerIdfallback behavior. This maintains backward compatibility for existing callers.
19-26: LGTM!The nullish coalescing fallback (
artifactKey ?? partnerId) correctly implements the documented default behavior, ensuring consistent groupKey generation whether an explicitartifactKeyis provided or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
π§Ή Nitpick comments (1)
apps/web/lib/api/fraud/utils.ts (1)
33-43: Clarify the artifactKey documentation.The documentation states artifactKey "can be" partnerId or other identifiers, but it's now the primary grouping mechanism (replacing partnerId). Consider updating the comment to emphasize that artifactKey is the definitive grouping identifier and provide examples of when to use partnerId vs. other keys (e.g., payout method ID for duplicate payout detection).
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π Files selected for processing (2)
apps/web/lib/api/fraud/resolve-fraud-events.ts(1 hunks)apps/web/lib/api/fraud/utils.ts(1 hunks)
π§° Additional context used
π§ Learnings (3)
π Common learnings
Learnt from: devkiran
Repo: dubinc/dub PR: 2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.693Z
Learning: In the Dub codebase, multiple payout records can now share the same stripeTransferId because payouts are grouped by partner and processed as single Stripe transfers. This is why the unique constraint was removed from the stripeTransferId field in the Payout model - a single transfer can include multiple payouts for the same partner.
π Learning: 2025-11-24T09:10:12.536Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3089
File: apps/web/lib/api/fraud/fraud-rules-registry.ts:17-25
Timestamp: 2025-11-24T09:10:12.536Z
Learning: In apps/web/lib/api/fraud/fraud-rules-registry.ts, the fraud rules `partnerCrossProgramBan` and `partnerDuplicatePayoutMethod` intentionally have stub implementations that return `{ triggered: false }` because they are partner-scoped rules handled separately during partner application/onboarding flows (e.g., in detect-record-fraud-application.ts), rather than being evaluated per conversion event like other rules in the registry. The stubs exist only to satisfy the `Record<FraudRuleType, ...>` type constraint.
Applied to files:
apps/web/lib/api/fraud/resolve-fraud-events.tsapps/web/lib/api/fraud/utils.ts
π Learning: 2025-08-14T05:17:51.825Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2735
File: apps/web/lib/actions/partners/delete-reward.ts:33-41
Timestamp: 2025-08-14T05:17:51.825Z
Learning: In the partner groups system, a rewardId can only belong to one group, establishing a one-to-one relationship between rewards and groups. This means using Prisma's `update` method (rather than `updateMany`) is appropriate when updating groups by rewardId.
Applied to files:
apps/web/lib/api/fraud/resolve-fraud-events.ts
𧬠Code graph analysis (1)
apps/web/lib/api/fraud/utils.ts (1)
packages/prisma/client.ts (1)
FraudRuleType(17-17)
β° 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 (2)
apps/web/lib/api/fraud/resolve-fraud-events.ts (1)
50-54: Verify the random artifactKey generation on resolution.The new groupKey is created with a random
nanoid(10)as the artifactKey. This means resolved events get a unique group key each time, isolating them from future events. However, this discards any original artifactKey context (e.g., payout method ID) that was used to group these events initially.Is this behavior intentional? If the goal is to archive resolved events under a unique identifier, this makes sense. Otherwise, consider whether the original artifactKey should be preserved or reused.
apps/web/lib/api/fraud/utils.ts (1)
29-31: LGTM!The hash key generation using SHA-256 and base64url encoding is appropriate for creating stable, collision-resistant group keys.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
β»οΈ Duplicate comments (1)
apps/web/lib/api/fraud/utils.ts (1)
29-31: Minor cleanups: drop unnecessary nonβnull assertion and confirmbase64urlsupportTwo small things here:
CreateGroupKeyInputmarks all fields as required, so the nonβnull assertion in.map((p) => p!.toLowerCase())is unnecessary. You can simplify to:-export function createFraudEventGroupKey(input: CreateGroupKeyInput): string { - const parts = [input.programId, input.type, input.groupingKey].map((p) => - p!.toLowerCase(), - ); +export function createFraudEventGroupKey(input: CreateGroupKeyInput): string { + const parts = [input.programId, input.type, input.groupingKey].map((p) => + p.toLowerCase(), + );
createHash("sha256").digest("base64url")depends on your Node runtime supporting the"base64url"encoding; if youβre still on an older Node, you may need to fall back to"base64"and post-process. Worth confirming against the deployed Node version.Also applies to: 33-52
π§Ή Nitpick comments (1)
apps/web/app/(ee)/api/stripe/connect/webhook/account-updated.ts (1)
5-5: Consider using a deterministic groupingKey instead ofnanoid(10)for this ruleRight now
groupingKeyis a freshnanoid(10)peraccountUpdatedrun, so repeated Stripeaccount.updateddeliveries or reprocessing for the samepayoutMethodHashwill generate newgroupKeys instead of reusing a stable grouping identifier. If you rely ongroupKeyfor idempotency/dedup ofpartnerDuplicatePayoutMethodevents, it may be safer to basegroupingKeyon the stablepayoutMethodHash(or similar) rather than a random value.Example adjustment:
- const groupingKey = nanoid(10); + const groupingKey = payoutMethodHash;That still gets hashed by
createFraudEventGroupKey, so the raw fingerprint isnβt exposed, but keeps grouping stable across retries for the same underlying payout method.Also applies to: 80-118
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π Files selected for processing (5)
apps/web/app/(ee)/api/stripe/connect/webhook/account-updated.ts(2 hunks)apps/web/lib/api/fraud/create-fraud-events.ts(1 hunks)apps/web/lib/api/fraud/detect-record-fraud-event.ts(1 hunks)apps/web/lib/api/fraud/resolve-fraud-events.ts(1 hunks)apps/web/lib/api/fraud/utils.ts(1 hunks)
π§ Files skipped from review as they are similar to previous changes (1)
- apps/web/lib/api/fraud/detect-record-fraud-event.ts
π§° Additional context used
π§ Learnings (6)
π Common learnings
Learnt from: devkiran
Repo: dubinc/dub PR: 2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.693Z
Learning: In the Dub codebase, multiple payout records can now share the same stripeTransferId because payouts are grouped by partner and processed as single Stripe transfers. This is why the unique constraint was removed from the stripeTransferId field in the Payout model - a single transfer can include multiple payouts for the same partner.
π Learning: 2025-07-11T16:28:55.693Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.693Z
Learning: In the Dub codebase, multiple payout records can now share the same stripeTransferId because payouts are grouped by partner and processed as single Stripe transfers. This is why the unique constraint was removed from the stripeTransferId field in the Payout model - a single transfer can include multiple payouts for the same partner.
Applied to files:
apps/web/app/(ee)/api/stripe/connect/webhook/account-updated.ts
π Learning: 2025-11-24T09:10:12.536Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3089
File: apps/web/lib/api/fraud/fraud-rules-registry.ts:17-25
Timestamp: 2025-11-24T09:10:12.536Z
Learning: In apps/web/lib/api/fraud/fraud-rules-registry.ts, the fraud rules `partnerCrossProgramBan` and `partnerDuplicatePayoutMethod` intentionally have stub implementations that return `{ triggered: false }` because they are partner-scoped rules handled separately during partner application/onboarding flows (e.g., in detect-record-fraud-application.ts), rather than being evaluated per conversion event like other rules in the registry. The stubs exist only to satisfy the `Record<FraudRuleType, ...>` type constraint.
Applied to files:
apps/web/app/(ee)/api/stripe/connect/webhook/account-updated.tsapps/web/lib/api/fraud/resolve-fraud-events.tsapps/web/lib/api/fraud/utils.tsapps/web/lib/api/fraud/create-fraud-events.ts
π Learning: 2025-07-17T06:41:45.620Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2637
File: apps/web/app/(ee)/api/singular/webhook/route.ts:0-0
Timestamp: 2025-07-17T06:41:45.620Z
Learning: In the Singular integration (apps/web/app/(ee)/api/singular/webhook/route.ts), the event names in the singularToDubEvent object have intentionally different casing: "Copy GAID" and "copy IDFA". This casing difference is valid and should not be changed, as these are the correct event names expected from Singular.
Applied to files:
apps/web/app/(ee)/api/stripe/connect/webhook/account-updated.ts
π Learning: 2025-08-14T05:17:51.825Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2735
File: apps/web/lib/actions/partners/delete-reward.ts:33-41
Timestamp: 2025-08-14T05:17:51.825Z
Learning: In the partner groups system, a rewardId can only belong to one group, establishing a one-to-one relationship between rewards and groups. This means using Prisma's `update` method (rather than `updateMany`) is appropriate when updating groups by rewardId.
Applied to files:
apps/web/lib/api/fraud/resolve-fraud-events.ts
π Learning: 2025-11-24T08:55:31.332Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3089
File: apps/web/app/(ee)/api/fraud-rules/route.ts:71-87
Timestamp: 2025-11-24T08:55:31.332Z
Learning: In apps/web/app/(ee)/api/fraud-rules/route.ts, fraud rules cannot be created in a disabled state. When using prisma.fraudRule.upsert, the create branch intentionally omits the disabledAt field (defaulting to null, meaning enabled), while the update branch allows toggling enabled/disabled state via the disabledAt field. This is a business logic constraint.
Applied to files:
apps/web/lib/api/fraud/create-fraud-events.ts
𧬠Code graph analysis (3)
apps/web/app/(ee)/api/stripe/connect/webhook/account-updated.ts (1)
apps/web/lib/api/fraud/create-fraud-events.ts (1)
createFraudEvents(13-39)
apps/web/lib/api/fraud/utils.ts (1)
packages/prisma/client.ts (1)
FraudRuleType(17-17)
apps/web/lib/api/fraud/create-fraud-events.ts (3)
packages/prisma/client.ts (1)
FraudRuleType(17-17)packages/prisma/index.ts (1)
prisma(3-9)apps/web/lib/api/fraud/utils.ts (1)
createFraudEventGroupKey(47-53)
β° 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 (2)
apps/web/lib/api/fraud/resolve-fraud-events.ts (1)
49-54: New random group key on resolve looks consistent with the grouping modelUsing
createFraudEventGroupKey({ programId, type, groupingKey: nanoid(10) })when resolving a group cleanly moves all events in that group to a newgroupKey, avoiding clashes with any future pending groups for the same rule. No issues from a correctness/consistency standpoint.apps/web/lib/api/fraud/create-fraud-events.ts (1)
2-2: GroupingKey fallback and typed input look solidThe new
CreateFraudEventsInputplusgroupingKey: groupingKey ?? partnerIdcleanly preserves existing behavior for callers that donβt provide a custom key while enabling more flexible grouping where needed. Centralizing groupKey computation throughcreateFraudEventGroupKeyand usingcreateId({ prefix: "fre_" })per event also keeps the creation path consistent and explicit. No changes suggested here.Also applies to: 6-11, 13-36
Summary by CodeRabbit
Bug Fixes
Refactor
New Features
βοΈ Tip: You can customize this high-level summary in your review settings.