Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useBulkRejectPartnersModal } from "@/ui/modals/bulk-reject-partners-mod
import { useRejectPartnerApplicationModal } from "@/ui/modals/reject-partner-application-modal";
import { GroupColorCircle } from "@/ui/partners/groups/group-color-circle";
import { PartnerApplicationSheet } from "@/ui/partners/partner-application-sheet";
import { PartnerApplicationSource } from "@/ui/partners/partner-application-source";
import { PartnerRowItem } from "@/ui/partners/partner-row-item";
import { PartnerSocialColumn } from "@/ui/partners/partner-social-column";
import { AnimatedEmptyState } from "@/ui/shared/animated-empty-state";
Expand Down Expand Up @@ -196,17 +197,16 @@ export function ProgramPartnersApplicationsPageClient() {
header: "Applied",
accessorFn: (d) => formatDate(d.createdAt, { month: "short" }),
},
// TODO: add source column back once we fix application source display
// {
// id: "source",
// header: "Source",
// minSize: 170,
// cell: ({ row }) => (
// <PartnerApplicationSource
// referralSource={row.original.applicationEvent?.referralSource}
// />
// ),
// },
{
id: "source",
header: "Source",
minSize: 170,
cell: ({ row }) => (
<PartnerApplicationSource
referralSource={row.original.applicationEvent?.referralSource}
/>
),
},
{
id: "group",
header: "Group",
Expand Down
4 changes: 3 additions & 1 deletion apps/web/lib/actions/partners/create-program-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,9 @@ async function createApplicationAndEnrollment({
},
}),

markApplicationEventSubmitted(programEnrollment),
markApplicationEventSubmitted(programEnrollment, {
partnerNetworkStatus: partner.networkStatus,
}),
]);
})(),
);
Expand Down
55 changes: 39 additions & 16 deletions apps/web/lib/application-events/update-application-event.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { prisma } from "@dub/prisma";
import { Prisma } from "@dub/prisma/client";
import { PartnerNetworkStatus, Prisma } from "@dub/prisma/client";
import { cookies } from "next/headers";
import { getApplicationEventCookieName } from "./utils";

// Application events visited as a guest have partnerId: null — look up the
// event via the browser cookie and backfill partnerId + submittedAt.
// Fallback to the (programId, partnerId) lookup if no cookie is present (e.g. the
// partner visited while already logged in on a different browser).
export async function markApplicationEventSubmitted({
programId,
partnerId,
applicationId,
}: Pick<
Prisma.ProgramEnrollmentCreateManyInput,
"programId" | "partnerId" | "applicationId"
>) {
export async function markApplicationEventSubmitted(
programEnrollment: Pick<
Prisma.ProgramEnrollmentCreateManyInput,
"programId" | "partnerId" | "applicationId"
>,
{ partnerNetworkStatus }: { partnerNetworkStatus: PartnerNetworkStatus },
) {
const { programId, partnerId, applicationId } = programEnrollment;
const cookieStore = await cookies();
const cookieName = getApplicationEventCookieName(programId);
const applicationEventId = cookieStore.get(cookieName)?.value;
Expand All @@ -25,21 +25,44 @@ export async function markApplicationEventSubmitted({
return;
}

const applicationEvent = await prisma.programApplicationEvent.findUnique({
where: {
...(applicationEventId
? { id: applicationEventId }
: { programId_partnerId: { programId, partnerId } }),
},
});

if (!applicationEvent) {
console.error(
"[markApplicationEventSubmitted]: No application event found, skipping...",
);
return;
Comment on lines +28 to +40

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fallback lookup is skipped when cookie ID is stale.

If applicationEventId exists but resolves to no row, the function returns early and never attempts the (programId, partnerId) unique lookup. That drops valid submission tracking for stale/deleted cookie IDs.

🔧 Proposed fix
-  const applicationEvent = await prisma.programApplicationEvent.findUnique({
-    where: {
-      ...(applicationEventId
-        ? { id: applicationEventId }
-        : { programId_partnerId: { programId, partnerId } }),
-    },
-  });
+  let applicationEvent = applicationEventId
+    ? await prisma.programApplicationEvent.findUnique({
+        where: { id: applicationEventId },
+      })
+    : null;
+
+  if (!applicationEvent && partnerId) {
+    applicationEvent = await prisma.programApplicationEvent.findUnique({
+      where: {
+        programId_partnerId: { programId, 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/lib/application-events/update-application-event.ts` around lines 28
- 40, The current lookup returns early if
prisma.programApplicationEvent.findUnique with applicationEventId yields no row,
skipping the fallback (programId_partnerId) lookup; update
markApplicationEventSubmitted so that when applicationEventId was provided but
findUnique returns null, it performs a second findUnique using {
programId_partnerId: { programId, partnerId } } before returning. In other
words, keep the initial attempt with applicationEventId (the call to
prisma.programApplicationEvent.findUnique), and if that result is null and
applicationEventId was present, run the fallback query for the (programId,
partnerId) composite key and only return/log when both queries fail.

}

try {
await prisma.programApplicationEvent.updateMany({
await prisma.programApplicationEvent.update({
where: {
...(applicationEventId
? { id: applicationEventId }
: { programId, partnerId }),
submittedAt: null,
id: applicationEvent.id,
},
data: {
partnerId,
submittedAt: new Date(),
partnerId,
programApplicationId: applicationId,
...(applicationEvent.referralSource === "marketplace" &&
!["approved", "trusted"].includes(partnerNetworkStatus)
? {
referralSource: "direct",
}
: {}),
},
});
Comment on lines +44 to 59

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Submission update lost its one-way transition guard.

update() now rewrites submittedAt on every retry/re-entry. This corrupts first-submission timestamps and can reapply referral-source rewrites. Guard the transition in the DB write (submittedAt: null) so it stays idempotent under retries/concurrency.

🔒 Proposed fix
-    await prisma.programApplicationEvent.update({
-      where: {
-        id: applicationEvent.id,
-      },
-      data: {
-        submittedAt: new Date(),
-        partnerId,
-        programApplicationId: applicationId,
-        ...(applicationEvent.referralSource === "marketplace" &&
-        !["approved", "trusted"].includes(partnerNetworkStatus)
-          ? {
-              referralSource: "direct",
-            }
-          : {}),
-      },
-    });
+    const { count } = await prisma.programApplicationEvent.updateMany({
+      where: {
+        id: applicationEvent.id,
+        submittedAt: null,
+      },
+      data: {
+        submittedAt: new Date(),
+        partnerId,
+        programApplicationId: applicationId,
+        ...(applicationEvent.referralSource === "marketplace" &&
+        !["approved", "trusted"].includes(partnerNetworkStatus)
+          ? { referralSource: "direct" }
+          : {}),
+      },
+    });
+
+    if (count === 0) {
+      return;
+    }

Based on learnings: the codebase prefers enforcing state-transition preconditions directly in Prisma where clauses.

📝 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
await prisma.programApplicationEvent.update({
where: {
...(applicationEventId
? { id: applicationEventId }
: { programId, partnerId }),
submittedAt: null,
id: applicationEvent.id,
},
data: {
partnerId,
submittedAt: new Date(),
partnerId,
programApplicationId: applicationId,
...(applicationEvent.referralSource === "marketplace" &&
!["approved", "trusted"].includes(partnerNetworkStatus)
? {
referralSource: "direct",
}
: {}),
},
});
const { count } = await prisma.programApplicationEvent.updateMany({
where: {
id: applicationEvent.id,
submittedAt: null,
},
data: {
submittedAt: new Date(),
partnerId,
programApplicationId: applicationId,
...(applicationEvent.referralSource === "marketplace" &&
!["approved", "trusted"].includes(partnerNetworkStatus)
? { referralSource: "direct" }
: {}),
},
});
if (count === 0) {
return;
}
🤖 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/application-events/update-application-event.ts` around lines 44
- 59, The current prisma.programApplicationEvent.update call rewrites
submittedAt and may reapply referralSource changes on retries; change the DB
write to enforce the one-way transition by adding submittedAt: null to the where
clause (e.g., use updateMany or an update with that predicate) so the update
only succeeds when submittedAt is still null, and keep the existing data payload
(submittedAt: new Date(), partnerId, programApplicationId, and the conditional
referralSource rewrite) so it remains idempotent under retries/concurrency;
after switching to updateMany, handle the returned count (zero means the
transition was already applied) if the caller needs to know.

} catch {}
} catch (error) {
console.error(
"[markApplicationEventSubmitted]: Error updating application event:",
error,
);
}
}

export async function trackApplicationEvents({
Expand Down
4 changes: 3 additions & 1 deletion apps/web/lib/partners/complete-program-applications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,9 @@ export async function completeProgramApplications(userEmail: string) {

await Promise.allSettled(
programEnrollments.map((programEnrollment) =>
markApplicationEventSubmitted(programEnrollment),
markApplicationEventSubmitted(programEnrollment, {
partnerNetworkStatus: partner.networkStatus,
}),
),
);
} catch (error) {
Expand Down
6 changes: 3 additions & 3 deletions apps/web/ui/partners/partner-info-cards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
PartnerFraudBanner,
} from "./fraud-risks/partner-fraud-banner";
import { PartnerFraudIndicator } from "./fraud-risks/partner-fraud-indicator";
import { PartnerApplicationSource } from "./partner-application-source";
import { PartnerAvatar } from "./partner-avatar";
import { PartnerInfoGroup } from "./partner-info-group";
import { PartnerStarButton } from "./partner-star-button";
Expand Down Expand Up @@ -175,16 +176,15 @@ export function PartnerInfoCards({
>
<span>Applied {formatDate(partner.createdAt)}</span>
</TimestampTooltip>
{/* TODO: add source column back once we fix application source display */}
{/* {applicationReferralSource && (
{applicationReferralSource && (
<>
<span>via</span>
<PartnerApplicationSource
referralSource={applicationReferralSource}
variant="inline"
/>
</>
)} */}
)}
</span>
) : (
`${isPendingApplication ? "Applied" : "Partner since"} ${formatDate(partner.createdAt)}`
Expand Down
Loading