Skip to content
Merged
Show file tree
Hide file tree
Changes from 235 commits
Commits
Show all changes
240 commits
Select commit Hold shift + click to select a range
42f116e
Move link settings under partner groups
steven-tey Aug 26, 2025
ce3596b
Update route.ts
steven-tey Aug 26, 2025
c1e992a
wip
devkiran Aug 26, 2025
70fefe9
Create default link sheet
devkiran Aug 26, 2025
0e194d0
Update add-edit-default-partner-link-sheet.tsx
devkiran Aug 26, 2025
e3c48fe
Update groups.ts
devkiran Aug 26, 2025
787404d
Merge branch 'main' into group-links
devkiran Aug 26, 2025
08411be
add PartnerLinkPreview
devkiran Aug 26, 2025
4e6b3e0
Add support for managing group default links
devkiran Aug 26, 2025
503bf58
Support default partner links for group enrollment
devkiran Aug 26, 2025
bc5ed9a
Update create-partner-link.ts
devkiran Aug 26, 2025
f536412
Add support for additional partner links in groups
devkiran Aug 26, 2025
f6a3b52
Refactor partner link creation and enrollment flow
devkiran Aug 26, 2025
7f7b21c
fix approvePartnerEnrollment
devkiran Aug 26, 2025
6e8c608
Update create-and-enroll-partner.ts
devkiran Aug 26, 2025
2974d4c
Update approve-partner-enrollment.ts
devkiran Aug 26, 2025
71e75f3
move to create-default-partner-links
devkiran Aug 26, 2025
430f14b
Enable editing for additional partner links
devkiran Aug 26, 2025
eada429
Update add-edit-additional-partner-link-modal.tsx
devkiran Aug 26, 2025
098c6ea
Update group-additional-links.tsx
devkiran Aug 26, 2025
7dda601
confirm delete operation
devkiran Aug 27, 2025
500da52
Update group-additional-links.tsx
devkiran Aug 27, 2025
571d88a
Update route.ts
devkiran Aug 27, 2025
8ead9cb
use link ID instead
devkiran Aug 27, 2025
1bfc9bc
Update group-default-links.tsx
devkiran Aug 27, 2025
6fb39cd
Add default link sync and diff utilities for groups
devkiran Aug 27, 2025
94427f3
Update route.ts
devkiran Aug 27, 2025
0d4fb20
Refactor partner link limits to use group settings
devkiran Aug 27, 2025
17b33c8
Refactor partner link validation to use group additionalLinks
devkiran Aug 27, 2025
2511df9
Update partner-link-modal.tsx
devkiran Aug 27, 2025
4317004
Update route.ts
devkiran Aug 27, 2025
b6e4f16
Add UTM template support to partner groups
devkiran Aug 27, 2025
5d0b15a
Update group-utm.tsx
devkiran Aug 27, 2025
c905bbb
Update get-group-or-throw.ts
devkiran Aug 27, 2025
90e16d3
Merge branch 'main' into group-links
devkiran Aug 27, 2025
b619d77
Add UTM sync cron route and update group PATCH logic
devkiran Aug 27, 2025
0e6dedb
Update route.ts
devkiran Aug 27, 2025
0cf64ee
Update group-utm.tsx
devkiran Aug 27, 2025
ac04ae4
Update route.ts
devkiran Aug 27, 2025
3dc59e7
Update page.tsx
devkiran Aug 27, 2025
e18386f
Update route.ts
devkiran Aug 27, 2025
50c4494
Implement default link update for partner groups
devkiran Aug 27, 2025
4ca27f0
Update add-edit-additional-partner-link-modal.tsx
devkiran Aug 27, 2025
20395a6
Update group-additional-links.tsx
devkiran Aug 27, 2025
ab265e1
Update groups.ts
devkiran Aug 27, 2025
951241a
Update route.ts
devkiran Aug 27, 2025
dbc8909
Add sync for updated additional partner links
devkiran Aug 27, 2025
b8b0051
Update add-edit-additional-partner-link-modal.tsx
devkiran Aug 27, 2025
5e4691e
cleanup
devkiran Aug 27, 2025
89864dd
Merge branch 'main' into group-links
TWilson023 Aug 27, 2025
05dde31
UI fixes/improvements
TWilson023 Aug 27, 2025
a4fdb00
Update layout.tsx
TWilson023 Aug 27, 2025
2d67ed6
Refactor default link sync logic for partner groups
devkiran Aug 28, 2025
4e7ad65
Update create-program.ts
devkiran Aug 28, 2025
4e0d4f4
Update bulk-approve-partners.ts
devkiran Aug 28, 2025
0c68fef
Update route.ts
devkiran Aug 28, 2025
23845a1
Update route.ts
devkiran Aug 28, 2025
b1a2713
fix TS
devkiran Aug 28, 2025
e60527c
fix build
devkiran Aug 28, 2025
e11f204
fix the imports
devkiran Aug 28, 2025
3ac1ae1
Update partners.ts
devkiran Aug 28, 2025
930b249
some fixes
devkiran Aug 28, 2025
8895110
Refactor additional partner link handling
devkiran Aug 28, 2025
79e876e
prevent updating the default partner link
devkiran Aug 28, 2025
89b6d15
Update validate-partner-link-url.ts
devkiran Aug 28, 2025
b4f8bdb
Update route.ts
devkiran Aug 28, 2025
37b02c0
Move linkStructure to the Group level.
devkiran Aug 28, 2025
d0a7fec
Check for duplicate destination URL
devkiran Aug 28, 2025
71558ae
Prevent deletion of UTM templates linked to groups
devkiran Aug 28, 2025
680931d
Add migration to backfill group link settings
devkiran Aug 28, 2025
5757e15
Remove id field from DefaultPartnerLink objects
devkiran Aug 28, 2025
4ddaafd
rename cron jobs
devkiran Aug 28, 2025
84f8d4d
Update route.ts
devkiran Aug 28, 2025
47f80ee
Update route.ts
devkiran Aug 28, 2025
efae227
Merge branch 'main' into group-links
devkiran Aug 28, 2025
5466475
final cleanup
devkiran Aug 28, 2025
1ed8c9d
rename
devkiran Aug 28, 2025
8e3b08c
Update create-and-enroll-partner.ts
devkiran Aug 28, 2025
f06063a
Update create-default-partner-links.ts
devkiran Aug 28, 2025
69fcaf1
Update route.ts
devkiran Aug 28, 2025
11e456c
Update validate-partner-link-url.ts
devkiran Aug 28, 2025
8e76386
Pass group to constructPartnerLink and update link logic
devkiran Aug 28, 2025
efd4e7b
Update utils.ts
devkiran Aug 28, 2025
67924a2
Update create-and-enroll-partner.ts
devkiran Aug 28, 2025
b172ea7
Update partner-application-sheet.tsx
devkiran Aug 28, 2025
513f84e
Remove webhookEnabled from workspace props
devkiran Aug 28, 2025
a3a4b4d
Update validate-partner-link-url.ts
devkiran Aug 28, 2025
dec789b
Update route.ts
devkiran Aug 28, 2025
f91d3e4
Update route.ts
devkiran Aug 28, 2025
a50dd44
add linkStructure to the link settings
devkiran Aug 28, 2025
8687c05
Update group-link-settings.tsx
devkiran Aug 28, 2025
f56326f
Update group-default-links.tsx
devkiran Aug 28, 2025
ec936b1
Merge branch 'main' into group-links
devkiran Aug 29, 2025
5ec9bc8
Merge branch 'main' into group-links
devkiran Aug 29, 2025
e7cf35c
add PartnerDefaultLink
devkiran Aug 29, 2025
e77d7be
update the table name
devkiran Aug 29, 2025
3d54817
Merge branch 'main' into group-links
steven-tey Aug 30, 2025
b2b9b11
Update group-link-settings.tsx
steven-tey Aug 30, 2025
05c88f5
Merge branch 'main' into group-links
steven-tey Aug 30, 2025
871ff71
Merge branch 'main' into group-links
steven-tey Aug 30, 2025
93bb974
refactor default links management
devkiran Aug 30, 2025
c9113e0
Merge branch 'group-links' of https://github.com/dubinc/dub into grou…
devkiran Aug 30, 2025
bf709e3
Update route.ts
devkiran Aug 30, 2025
8849d6b
Update group-default-links.tsx
devkiran Aug 30, 2025
1146095
API - Partner shouldn't be able to update the default links
devkiran Aug 30, 2025
45c58e5
Update backfill-groups-link-settings.ts
devkiran Aug 30, 2025
f2fbb9d
Update route.ts
devkiran Aug 30, 2025
0100861
Update route.ts
devkiran Aug 30, 2025
ff3eed7
Update route.ts
devkiran Aug 30, 2025
783403f
Update route.ts
devkiran Aug 30, 2025
fceb828
Fix the sync utm cron job
devkiran Aug 30, 2025
410dfc5
Refactor partner link creation and improve UTM handling
devkiran Aug 30, 2025
3b7ee5e
more fixes related to the new default link setup
devkiran Aug 30, 2025
93dfaa9
make sure utm is added to the link
devkiran Aug 30, 2025
70afee3
Update links-list.tsx
devkiran Aug 30, 2025
6d8c384
defaultLinks cleanups
devkiran Aug 30, 2025
031fd57
Update route.ts
devkiran Aug 30, 2025
a0c2e8c
Update route.ts
devkiran Aug 30, 2025
4ae078e
Update route.ts
devkiran Aug 30, 2025
9ca78c1
Update route.ts
devkiran Aug 30, 2025
93951ae
fix the cron jobs
devkiran Aug 30, 2025
473925a
Update route.ts
devkiran Aug 30, 2025
554a1a4
Update route.ts
devkiran Aug 30, 2025
a44f96e
Update route.ts
devkiran Aug 30, 2025
c748000
Update route.ts
devkiran Aug 30, 2025
68e8a73
Update route.ts
devkiran Aug 30, 2025
0fa2d7d
Merge branch 'main' into group-links
devkiran Aug 31, 2025
10c4611
Merge branch 'main' into group-links
steven-tey Aug 31, 2025
14f3098
Merge branch 'main' into group-links
steven-tey Aug 31, 2025
c204ef9
Update route.ts
devkiran Aug 31, 2025
14e7ee6
Update route.ts
devkiran Aug 31, 2025
adc3d5f
Update route.ts
devkiran Aug 31, 2025
0551172
Merge branch 'group-links' of https://github.com/dubinc/dub into grou…
devkiran Aug 31, 2025
8bc4a6a
Update route.ts
devkiran Aug 31, 2025
2aac3ed
Update route.ts
devkiran Aug 31, 2025
76e4b78
format
devkiran Aug 31, 2025
84155d8
Update route.ts
devkiran Aug 31, 2025
7bb6cb7
some cleanup
devkiran Aug 31, 2025
34004dd
If there is only one destination domain and we are in exact mode, hid…
devkiran Aug 31, 2025
5db029d
add canCreateNewLink
devkiran Aug 31, 2025
ccac033
fix button height
devkiran Aug 31, 2025
1cd61a6
Update partner-link-modal.tsx
devkiran Aug 31, 2025
2a8a002
Update partner-link-modal.tsx
devkiran Aug 31, 2025
31bf7e5
Update route.ts
devkiran Aug 31, 2025
3b1d29e
display destination domain dropdown
devkiran Aug 31, 2025
d1d27d5
Merge branch 'main' into group-links
devkiran Sep 1, 2025
18bbe4a
simplify the logic
devkiran Sep 1, 2025
0598d44
Update create-partner-default-links.ts
devkiran Sep 1, 2025
4d6b093
Remove partnerGroupDefaultLinkId from all /links/** API response
devkiran Sep 1, 2025
36a6e2c
Disable the domain dropdown in the create/edit default link sheet
devkiran Sep 1, 2025
469007a
add remapPartnerGroupDefaultLinks
devkiran Sep 1, 2025
aed935c
Update route.ts
devkiran Sep 1, 2025
8d7c3cb
format
devkiran Sep 1, 2025
2901ac9
Update links-list.tsx
devkiran Sep 1, 2025
37a6254
Update add-edit-link.tsx
devkiran Sep 1, 2025
a524570
Merge branch 'main' into group-links
steven-tey Sep 2, 2025
d74107d
Merge branch 'main' into group-links
steven-tey Sep 2, 2025
53d6df4
Merge branch 'main' into group-links
devkiran Sep 3, 2025
2371786
Merge branch 'main' into group-links
devkiran Sep 3, 2025
7963f3c
Merge branch 'group-links' of https://github.com/dubinc/dub into grou…
devkiran Sep 3, 2025
6e3e9c6
Merge branch 'main' into group-links
steven-tey Sep 3, 2025
b9784ee
Merge branch 'main' into group-links
steven-tey Sep 3, 2025
142f7f2
Merge branch 'main' into group-links
steven-tey Sep 4, 2025
ef1c816
Merge branch 'main' into group-links
steven-tey Sep 5, 2025
5b7050d
Merge branch 'main' into group-links
devkiran Sep 7, 2025
7d0e90e
Merge branch 'main' into group-links
steven-tey Sep 8, 2025
4f490a7
Merge branch 'main' into group-links
steven-tey Sep 9, 2025
8b63f76
Merge branch 'main' into group-links
steven-tey Sep 10, 2025
1d03009
Merge branch 'main' into group-links
devkiran Sep 10, 2025
9507acd
Merge branch 'main' into group-links
steven-tey Sep 10, 2025
5412d5d
Merge branch 'main' into group-links
steven-tey Sep 10, 2025
8d8aece
Merge branch 'main' into group-links
steven-tey Sep 11, 2025
bdd0752
make script idempotent, add unique constraint for groupId <> url, sma…
steven-tey Sep 11, 2025
4fd63c3
Merge branch 'main' into group-links
steven-tey Sep 11, 2025
d595100
Merge branch 'main' into group-links
steven-tey Sep 11, 2025
4d37bc2
Merge branch 'main' into group-links
steven-tey Sep 12, 2025
aa2a228
simplify og/program/route
steven-tey Sep 12, 2025
445d0f2
more changes
steven-tey Sep 12, 2025
8e5d061
small fixes to PartnerLinkModal
steven-tey Sep 12, 2025
446558b
update additionalPartnerLinkSchema
devkiran Sep 12, 2025
b88259f
Update validate-partner-link-url.ts
devkiran Sep 12, 2025
00d150c
format
devkiran Sep 12, 2025
80d0161
Update partner-link-modal.tsx
devkiran Sep 12, 2025
2ef7ab8
Update add-edit-link.tsx
devkiran Sep 12, 2025
5ff6b34
deprecate old props
steven-tey Sep 12, 2025
5b39c83
small changes
steven-tey Sep 12, 2025
21ef882
disable the domain selector during link edit
devkiran Sep 12, 2025
bf556de
Merge branch 'group-links' of https://github.com/dubinc/dub into grou…
devkiran Sep 12, 2025
e883f3e
Update partner-link-modal.tsx
devkiran Sep 12, 2025
b85845c
Update partner-link-modal.tsx
devkiran Sep 12, 2025
08bce46
small fix
devkiran Sep 12, 2025
6b10669
fix constructPartnerLink
steven-tey Sep 12, 2025
fa3e405
Merge branch 'group-links' of https://github.com/dubinc/dub into grou…
devkiran Sep 12, 2025
f464029
simplify
steven-tey Sep 12, 2025
5357f9b
Merge branch 'group-links' of https://github.com/dubinc/dub into grou…
steven-tey Sep 12, 2025
d9365a5
fix the exact domain validation
devkiran Sep 12, 2025
9fbf7ab
Merge branch 'group-links' of https://github.com/dubinc/dub into grou…
devkiran Sep 12, 2025
ff429cc
Update program-card.tsx
steven-tey Sep 12, 2025
2254060
Update add-partner-link-modal.tsx
devkiran Sep 12, 2025
1205b31
Merge branch 'group-links' of https://github.com/dubinc/dub into grou…
devkiran Sep 12, 2025
470a85d
fix adding & editing partner links
devkiran Sep 12, 2025
56fa1dd
Update programs.ts
devkiran Sep 12, 2025
fa30c81
final changes
steven-tey Sep 12, 2025
afefae5
fix validatePartnerLinkUrl
steven-tey Sep 12, 2025
76faea6
remove redundant throw
steven-tey Sep 12, 2025
c73d73f
fix sync-utm
steven-tey Sep 12, 2025
99784c8
move remap-default-links to a cron
steven-tey Sep 12, 2025
f1eda26
finalize remap-default-links to make it bulletproof
steven-tey Sep 12, 2025
bb23650
small tweaks
steven-tey Sep 12, 2025
35f2a58
finalize PATCH utm route
steven-tey Sep 12, 2025
6584b99
remove redundant invalidate-for-discounts
steven-tey Sep 12, 2025
1a269d8
extractUtmParams
steven-tey Sep 12, 2025
56e449e
a lot more changes to make things bulletproof
steven-tey Sep 12, 2025
f6a3225
missed a few spots
steven-tey Sep 12, 2025
9e413ff
update copy
steven-tey Sep 12, 2025
8c93f3e
Update group-link-settings.tsx
steven-tey Sep 12, 2025
27a681c
fix mutate
steven-tey Sep 12, 2025
300a50c
address coderabbit feedback
steven-tey Sep 13, 2025
3c938f7
onDelete: NoAction
steven-tey Sep 13, 2025
9dba500
fix bugs in remapPartnerGroupDefaultLinks
steven-tey Sep 13, 2025
d60d2c6
fix sync-utm to be groupId based instead
steven-tey Sep 13, 2025
bd49fa3
Merge branch 'main' into group-links
steven-tey Sep 13, 2025
c55edab
Update route.ts
devkiran Sep 13, 2025
f594e95
update UTM param is the template is newly created for a group
devkiran Sep 13, 2025
5e1f430
Update delete confirmation modal to display URL without UTM parameters
devkiran Sep 13, 2025
5b2033b
Merge branch 'main' into group-links
steven-tey Sep 14, 2025
8c76623
fix a bunch of stuff to use extractUtmParams
steven-tey Sep 14, 2025
1452ac7
make sure to apply utmTemplate for /partners/links endpoints
steven-tey Sep 14, 2025
42c766d
update E2E tests
steven-tey Sep 14, 2025
c44099d
add isGroupDeleted to handle cascade partnerGroupDefaultLinkId SetNu…
steven-tey Sep 14, 2025
fc09df3
handle duplicate default links, add normalizeUrl utility fn
steven-tey Sep 14, 2025
9c4c9aa
make prefix mandatory in createId
steven-tey Sep 14, 2025
b341e5e
Update extract-utm-params.ts
steven-tey Sep 14, 2025
ef6696f
fix AddPartnerLinkModal, improve extractUtmParams types
steven-tey Sep 14, 2025
141c23e
fix typings
steven-tey Sep 14, 2025
a066e6c
fix MAX_ADDITIONAL_PARTNER_LINKS
steven-tey Sep 14, 2025
74f1200
Merge branch 'main' into group-links
steven-tey Sep 14, 2025
27a72bf
Update backfill-group-links-pgdl.ts
steven-tey Sep 14, 2025
982b261
Update backfill-group-links-pgdl.ts
steven-tey Sep 14, 2025
2f80a8e
fix ts
steven-tey Sep 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/app/(ee)/api/bounties/[bountyId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ export const PATCH = withWorkspace(
},
],
}),

sendWorkspaceWebhook({
workspace,
trigger: "bounty.updated",
Expand Down
8 changes: 5 additions & 3 deletions apps/web/app/(ee)/api/cron/auto-approve-partner/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,19 @@ export async function POST(req: Request) {
);
}

if (program.partners[0].status !== "pending") {
const partner = program.partners[0];

if (partner.status !== "pending") {
return new Response(
`${partnerId} is ${program.partners[0].status}. Skipping auto-approval.`,
`${partnerId} is ${partner.status}. Skipping auto-approval.`,
);
}

await approvePartnerEnrollment({
programId,
partnerId,
linkId: null,
userId: program.workspace.users[0].userId,
groupId: partner.groupId,
});

return new Response("Partner is auto-approved.");
Expand Down
198 changes: 198 additions & 0 deletions apps/web/app/(ee)/api/cron/groups/create-default-links/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { handleAndReturnErrorResponse } from "@/lib/api/errors";
import { bulkCreateLinks } from "@/lib/api/links";
import { generatePartnerLink } from "@/lib/api/partners/generate-partner-link";
import { extractUtmParams } from "@/lib/api/utm/extract-utm-params";
import { qstash } from "@/lib/cron";
import { verifyQstashSignature } from "@/lib/cron/verify-qstash";
import { WorkspaceProps } from "@/lib/types";
import { prisma } from "@dub/prisma";
import {
APP_DOMAIN_WITH_NGROK,
constructURLFromUTMParams,
isFulfilled,
log,
} from "@dub/utils";
import { z } from "zod";
import { logAndRespond } from "../../utils";
export const dynamic = "force-dynamic";

const PAGE_SIZE = 100;
const MAX_BATCH = 10;

const schema = z.object({
defaultLinkId: z.string(),
userId: z.string(),
cursor: z.string().optional(),
});

/**
* Cron job to create default partner links for all approved partners in a group.
*
* For each approved partner in the group, it creates a link based on
* the group's default link configuration (domain, URL, etc.).
*
* It processes up to MAX_BATCH * PAGE_SIZE partners per execution
* and schedules additional jobs if needed.
*/

// POST /api/cron/groups/create-default-links
export async function POST(req: Request) {
try {
const rawBody = await req.text();
await verifyQstashSignature({ req, rawBody });

const { defaultLinkId, userId, cursor } = schema.parse(JSON.parse(rawBody));

// Find the default link
const defaultLink = await prisma.partnerGroupDefaultLink.findUnique({
where: {
id: defaultLinkId,
},
include: {
partnerGroup: {
include: {
utmTemplate: true,
},
},
},
});

if (!defaultLink) {
return logAndRespond(
`Default link ${defaultLinkId} not found. Skipping...`,
{
logLevel: "error",
},
);
}

const group = defaultLink.partnerGroup;
if (!group) {
return logAndRespond(
`Group ${defaultLink.groupId} not found. Skipping...`,
{
logLevel: "error",
},
);
}

console.info(
`Creating default links for the partners (defaultLinkId=${defaultLink.id}, groupId=${group.id}).`,
);

// Find the workspace & program
const { workspace, ...program } = await prisma.program.findUniqueOrThrow({
where: {
id: group.programId,
},
include: {
workspace: true,
},
});

const { utmTemplate } = group;

let hasMore = true;
let currentCursor = cursor;
let processedBatches = 0;

while (processedBatches < MAX_BATCH) {
// Find partners in the group
const programEnrollments = await prisma.programEnrollment.findMany({
where: {
...(currentCursor && {
id: {
gt: currentCursor,
},
}),
groupId: group.id,
status: "approved",
},
include: {
partner: true,
},
take: PAGE_SIZE,
orderBy: {
id: "asc",
},
});
Comment on lines +99 to +118
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Use cursor pagination instead of id: { gt: cursor } to avoid ordering issues.

If ProgramEnrollment.id is non-monotonic (e.g., CUID), gt is unsafe. Use Prisma cursor + skip: 1 with orderBy.

-      const programEnrollments = await prisma.programEnrollment.findMany({
-        where: {
-          ...(currentCursor && {
-            id: {
-              gt: currentCursor,
-            },
-          }),
-          groupId: group.id,
-          status: "approved",
-        },
+      const programEnrollments = await prisma.programEnrollment.findMany({
+        where: {
+          groupId: group.id,
+          status: "approved",
+        },
         include: {
           partner: true,
         },
-        take: PAGE_SIZE,
-        orderBy: {
-          id: "asc",
-        },
+        orderBy: { id: "asc" },
+        take: PAGE_SIZE,
+        ...(currentCursor && { cursor: { id: currentCursor }, skip: 1 }),
       });

To confirm model types, run:


🏁 Script executed:

#!/bin/bash
# Inspect Prisma models to confirm ID types for Link and ProgramEnrollment
fd -a '*.prisma' packages/prisma | while read -r f; do
  echo "--- $f ---"
  sed -n '/model Link\b/,/}/p;/model ProgramEnrollment\b/,/}/p' "$f"
done

Length of output: 334


🏁 Script executed:

#!/bin/bash
# Inspect Prisma models to confirm ID types for Link and ProgramEnrollment
find packages/prisma -type f -name '*.prisma' -print0 | while IFS= read -r -d '' f; do
  echo "--- $f ---"
  sed -n '/model Link\b/,/}/p;/model ProgramEnrollment\b/,/}/p' "$f"
done

Length of output: 12402


Use Prisma cursor pagination for ProgramEnrollment
ProgramEnrollment.id is defined as String @id @default(cuid()), so filtering with gt on CUIDs can skip or duplicate records. Replace with cursor + skip: 1:

-      const programEnrollments = await prisma.programEnrollment.findMany({
-        where: {
-          ...(currentCursor && {
-            id: {
-              gt: currentCursor,
-            },
-          }),
-          groupId: group.id,
-          status: "approved",
-        },
+      const programEnrollments = await prisma.programEnrollment.findMany({
+        where: {
+          groupId: group.id,
+          status: "approved",
+        },
         include: {
           partner: true,
         },
-        take: PAGE_SIZE,
-        orderBy: {
-          id: "asc",
-        },
+        orderBy: { id: "asc" },
+        take: PAGE_SIZE,
+        ...(currentCursor && { cursor: { id: currentCursor }, skip: 1 }),
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
while (processedBatches < MAX_BATCH) {
// Find partners in the group
const programEnrollments = await prisma.programEnrollment.findMany({
where: {
...(currentCursor && {
id: {
gt: currentCursor,
},
}),
groupId: group.id,
status: "approved",
},
include: {
partner: true,
},
take: PAGE_SIZE,
orderBy: {
id: "asc",
},
});
while (processedBatches < MAX_BATCH) {
// Find partners in the group
const programEnrollments = await prisma.programEnrollment.findMany({
where: {
groupId: group.id,
status: "approved",
},
include: {
partner: true,
},
orderBy: { id: "asc" },
take: PAGE_SIZE,
...(currentCursor && { cursor: { id: currentCursor }, skip: 1 }),
});
🤖 Prompt for AI Agents
In apps/web/app/(ee)/api/cron/groups/create-default-links/route.ts around lines
90 to 109, the query uses an id gt filter against a CUID which can cause
skipped/duplicated rows; change to Prisma cursor pagination by: when
currentCursor is present pass { cursor: { id: currentCursor }, skip: 1 } into
the findMany instead of the id.gt filter, remove the id.gt block, keep take:
PAGE_SIZE and orderBy: { id: "asc" }, and ensure currentCursor is the
ProgramEnrollment.id string (or convert to string) so the cursor is valid.


if (programEnrollments.length === 0) {
hasMore = false;
break;
}

// Create a new defaultLink for each partner in the group
const processedLinks = (
await Promise.allSettled(
programEnrollments.map(({ partner, ...programEnrollment }) =>
generatePartnerLink({
workspace: {
id: workspace.id,
plan: workspace.plan as WorkspaceProps["plan"],
},
program: {
id: program.id,
defaultFolderId: program.defaultFolderId,
},
partner: {
id: partner.id,
name: partner.name,
email: partner.email!,
Copy link
Contributor

Choose a reason for hiding this comment

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

The code uses partner.email! (line 137) to force unwrap a potentially null email field, but the Prisma query doesn't filter out partners with null emails, causing runtime crashes.

View Details
📝 Patch Details
diff --git a/apps/web/app/(ee)/api/cron/groups/create-default-links/route.ts b/apps/web/app/(ee)/api/cron/groups/create-default-links/route.ts
index c454e8a6b..4932ff061 100644
--- a/apps/web/app/(ee)/api/cron/groups/create-default-links/route.ts
+++ b/apps/web/app/(ee)/api/cron/groups/create-default-links/route.ts
@@ -103,6 +103,11 @@ export async function POST(req: Request) {
           }),
           groupId: group.id,
           status: "approved",
+          partner: {
+            email: {
+              not: null,
+            },
+          },
         },
         include: {
           partner: true,
diff --git a/apps/web/app/(ee)/api/groups/[groupIdOrSlug]/partners/route.ts b/apps/web/app/(ee)/api/groups/[groupIdOrSlug]/partners/route.ts
index 739e4bd7d..4d67524cb 100644
--- a/apps/web/app/(ee)/api/groups/[groupIdOrSlug]/partners/route.ts
+++ b/apps/web/app/(ee)/api/groups/[groupIdOrSlug]/partners/route.ts
@@ -57,6 +57,11 @@ export const POST = withWorkspace(
             in: partnerIds,
           },
           programId,
+          partner: {
+            email: {
+              not: null,
+            },
+          },
         },
         include: {
           partner: true,

Analysis

Runtime error in generatePartnerLink() when partner email is null

What fails: The derivePartnerLinkKey() function in generate-partner-link.ts calls email.split("@")[0] on null email values, causing "Cannot read properties of null (reading 'split')" runtime errors.

How to reproduce:

# Create a partner with null email and trigger default link creation
# The cron job queries partners without filtering null emails, then passes null to generatePartnerLink

Result: TypeError: Cannot read properties of null (reading 'split') when derivePartnerLinkKey processes partners with null emails

Expected: Should filter out partners with null emails in the Prisma query, following the same pattern as notify-partners cron job which uses partner: { email: { not: null } }

tenantId: programEnrollment.tenantId ?? undefined,
},
Comment on lines +138 to +143
Copy link
Contributor

Choose a reason for hiding this comment

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

The partner object passed to generatePartnerLink is missing the username field, which will cause the derivePartnerLinkKey function to fail when trying to access partner.username.

View Details
📝 Patch Details
diff --git a/apps/web/app/(ee)/api/cron/groups/create-default-links/route.ts b/apps/web/app/(ee)/api/cron/groups/create-default-links/route.ts
index c454e8a6b..e070be3fd 100644
--- a/apps/web/app/(ee)/api/cron/groups/create-default-links/route.ts
+++ b/apps/web/app/(ee)/api/cron/groups/create-default-links/route.ts
@@ -135,6 +135,7 @@ export async function POST(req: Request) {
                 id: partner.id,
                 name: partner.name,
                 email: partner.email!,
+                username: partner.username,
                 tenantId: programEnrollment.tenantId ?? undefined,
               },
               link: {

Analysis

In the cron job that creates default partner links, the partner object being passed to generatePartnerLink on lines 134-139 only includes id, name, email, and tenantId. However, the generatePartnerLink function expects the partner to conform to the CreatePartnerProps interface (line 16 in generate-partner-link.ts), which includes a username field.

The derivePartnerLinkKey function (lines 92-116 in generate-partner-link.ts) expects to access partner.username on line 34, but this field is not being provided in the cron job. This will result in partner.username being undefined, which could cause unexpected behavior in link key generation.

The partner object should include the username field from the database. This can be fixed by either:

  1. Including username in the partner select from programEnrollment.partner in the database query on line 108
  2. Or explicitly setting username: partner.username ?? null in the partner object construction

link: {
domain: defaultLink.domain,
url: constructURLFromUTMParams(
defaultLink.url,
extractUtmParams(group.utmTemplate),
),
...extractUtmParams(group.utmTemplate, { excludeRef: true }),
tenantId: programEnrollment.tenantId ?? undefined,
partnerGroupDefaultLinkId: defaultLink.id,
},
userId,
}),
),
)
)
.filter(isFulfilled)
.map(({ value }) => value);

const createdLinks = await bulkCreateLinks({
links: processedLinks,
});

console.log(
`Created ${createdLinks.length} default links for the partners in the group ${group.id}.`,
);

// Update cursor to the last processed record
currentCursor = programEnrollments[programEnrollments.length - 1].id;
processedBatches++;
}

if (hasMore) {
await qstash.publishJSON({
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/groups/create-default-links`,
method: "POST",
body: {
defaultLinkId,
userId,
cursor: currentCursor,
},
});
}

return logAndRespond(`Finished creating default links for the partners.`);
} catch (error) {
await log({
message: `Error creating default links for the partners: ${error.message}.`,
type: "errors",
});

console.error(error);

return handleAndReturnErrorResponse(error);
}
}
Loading