Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
16 changes: 9 additions & 7 deletions apps/web/lib/actions/fraud/bulk-resolve-fraud-groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const bulkResolveFraudGroupsAction = authActionClient
id: true,
programId: true,
partnerId: true,
type: true,
},
});

Expand Down Expand Up @@ -66,18 +67,19 @@ export const bulkResolveFraudGroupsAction = authActionClient
...(resolutionReason && { resolutionReason }),
});

// Add the resolution reason as a comment to each unique partner
// Add the resolution reason as a comment for each resolved fraud group
if (resolutionReason && count > 0) {
const uniquePartnerIds = Array.from(
new Set(fraudGroups.map((group) => group.partnerId)),
);

await prisma.partnerComment.createMany({
data: uniquePartnerIds.map((partnerId) => ({
data: fraudGroups.map((group) => ({
programId,
partnerId,
partnerId: group.partnerId,
userId: user.id,
text: resolutionReason,
metadata: {
source: "fraudResolution",
groupId: group.id,
type: group.type,
},
})),
});
}
Expand Down
6 changes: 6 additions & 0 deletions apps/web/lib/actions/fraud/resolve-fraud-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const resolveFraudGroupAction = authActionClient
id: true,
programId: true,
partnerId: true,
type: true,
},
});

Expand All @@ -60,6 +61,11 @@ export const resolveFraudGroupAction = authActionClient
partnerId: fraudGroup.partnerId,
userId: user.id,
text: resolutionReason,
metadata: {
source: "fraudResolution",
groupId: fraudGroup.id,
type: fraudGroup.type,
},
},
});
}
Expand Down
21 changes: 21 additions & 0 deletions apps/web/lib/actions/partners/update-partner-comment.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
"use server";

import {
parseFraudResolutionComment,
} from "@/lib/fraud-resolution-comment";
import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw";
import { prisma } from "@dub/prisma";
import {
Expand All @@ -23,6 +26,23 @@ export const updatePartnerCommentAction = authActionClient

const programId = getDefaultProgramIdOrThrow(workspace);

// Preserve fraud resolution metadata from the existing comment
const existing = await prisma.partnerComment.findUniqueOrThrow({
where: { id, programId, userId: user.id },
select: { text: true, metadata: true },
});

const parsedLegacy = parseFraudResolutionComment(existing.text);
const metadata =
existing.metadata ??
(parsedLegacy.metadata
? {
source: "fraudResolution",
groupId: parsedLegacy.metadata.groupId,
type: parsedLegacy.metadata.type,
}
: null);

const comment = await prisma.partnerComment.update({
where: {
id,
Expand All @@ -31,6 +51,7 @@ export const updatePartnerCommentAction = authActionClient
},
data: {
text,
metadata,
},
include: {
user: true,
Expand Down
73 changes: 73 additions & 0 deletions apps/web/lib/fraud-resolution-comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type { FraudRuleType } from "@dub/prisma/client";

const FRAUD_RESOLUTION_COMMENT_PREFIX = "[[dub-fraud-resolution:";
const FRAUD_RESOLUTION_COMMENT_SUFFIX = "]]";

export interface FraudResolutionCommentMetadata {
groupId: string;
type: FraudRuleType;
}

export function serializeFraudResolutionComment({
metadata,
note,
}: {
metadata: FraudResolutionCommentMetadata;
note: string;
}) {
return `${FRAUD_RESOLUTION_COMMENT_PREFIX}${JSON.stringify(metadata)}${FRAUD_RESOLUTION_COMMENT_SUFFIX}\n${note}`;
}

export function parseFraudResolutionComment(text: string): {
metadata: FraudResolutionCommentMetadata | null;
note: string;
} {
if (!text.startsWith(FRAUD_RESOLUTION_COMMENT_PREFIX)) {
return {
metadata: null,
note: text,
};
}

const suffixIndex = text.indexOf(FRAUD_RESOLUTION_COMMENT_SUFFIX);

if (suffixIndex === -1) {
return {
metadata: null,
note: text,
};
}

const metadataString = text.slice(
FRAUD_RESOLUTION_COMMENT_PREFIX.length,
suffixIndex,
);

try {
const metadata = JSON.parse(metadataString) as FraudResolutionCommentMetadata;

if (
!metadata ||
typeof metadata.groupId !== "string" ||
typeof metadata.type !== "string"
) {
return {
metadata: null,
note: text,
};
}

const contentStart = suffixIndex + FRAUD_RESOLUTION_COMMENT_SUFFIX.length;
const hasNewLine = text.charAt(contentStart) === "\n";

return {
metadata,
note: text.slice(hasNewLine ? contentStart + 1 : contentStart),
};
} catch {
return {
metadata: null,
note: text,
};
}
}
8 changes: 8 additions & 0 deletions apps/web/lib/zod/schemas/programs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import {
Category,
EventType,
FraudRuleType,
PartnerBannedReason,
ProgramEnrollmentStatus,
ProgramPayoutMode,
Expand Down Expand Up @@ -188,6 +189,12 @@ export const createProgramApplicationSchema = z.object({
formData: programApplicationFormDataWithValuesSchema,
});

export const fraudResolutionCommentMetadataSchema = z.object({
source: z.literal("fraudResolution"),
groupId: z.string(),
type: z.enum(FraudRuleType),
});

export const PartnerCommentSchema = z.object({
id: z.string(),
programId: z.string(),
Expand All @@ -199,6 +206,7 @@ export const PartnerCommentSchema = z.object({
image: true,
}),
text: z.string(),
metadata: z.unknown().nullable().default(null),
createdAt: z.date(),
updatedAt: z.date(),
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { parseFraudResolutionComment } from "@/lib/fraud-resolution-comment";
import { prisma } from "@dub/prisma";
import "dotenv-flow/config";

async function main() {
const dryRun = process.argv.includes("--dry-run");

const comments = await prisma.partnerComment.findMany({
where: {
metadata: null,
},
select: {
id: true,
programId: true,
partnerId: true,
userId: true,
text: true,
createdAt: true,
},
orderBy: {
createdAt: "asc",
},
});

let upgradedFromLegacyEncoded = 0;
let upgradedFromResolvedGroupMatch = 0;
let ambiguous = 0;
let unmatched = 0;

for (const comment of comments) {
const legacy = parseFraudResolutionComment(comment.text);

if (legacy.metadata) {
if (!dryRun) {
await prisma.partnerComment.update({
where: { id: comment.id },
data: {
text: legacy.note,
metadata: {
source: "fraudResolution",
groupId: legacy.metadata.groupId,
type: legacy.metadata.type,
},
},
});
}

upgradedFromLegacyEncoded++;
continue;
}

const matchingGroups = await prisma.fraudEventGroup.findMany({
where: {
programId: comment.programId,
partnerId: comment.partnerId,
userId: comment.userId,
status: "resolved",
resolutionReason: comment.text,
},
select: {
id: true,
type: true,
resolvedAt: true,
},
});

if (matchingGroups.length === 0) {
unmatched++;
continue;
}

if (matchingGroups.length > 1) {
ambiguous++;
continue;
}

const [group] = matchingGroups;

if (!dryRun) {
await prisma.partnerComment.update({
where: { id: comment.id },
data: {
metadata: {
source: "fraudResolution",
groupId: group.id,
type: group.type,
},
},
});
}

upgradedFromResolvedGroupMatch++;
}

console.log(
[
`Scanned ${comments.length} comments with null metadata`,
`Upgraded from legacy encoded comments: ${upgradedFromLegacyEncoded}`,
`Upgraded from fraud group match: ${upgradedFromResolvedGroupMatch}`,
`Ambiguous matches skipped: ${ambiguous}`,
`No matches found: ${unmatched}`,
`Mode: ${dryRun ? "dry-run" : "write"}`,
].join("\n"),
);
}

main();
Loading