Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -152,14 +152,14 @@ export async function createNewCustomer(event: Stripe.Event) {
},
},
}),

syncPartnerLinksStats({
partnerId: link.partnerId,
programId: link.programId,
eventType: "lead",
}),

webhookPartner &&
commission &&
detectAndRecordFraudEvent({
program: { id: link.programId },
partner: pick(webhookPartner, ["id", "email", "name"]),
Expand Down
19 changes: 10 additions & 9 deletions apps/web/lib/api/conversions/track-lead.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,15 +332,16 @@ export const trackLead = async ({
eventType: "lead",
}),

detectAndRecordFraudEvent({
program: { id: link.programId },
partner: pick(webhookPartner, ["id", "email", "name"]),
customer: pick(customer, ["id", "email", "name"]),
commission: { id: createdCommission.commission?.id },
link: pick(link, ["id"]),
click: pick(clickData, ["url", "referer"]),
event: { id: leadEventId },
}),
webhookPartner &&
detectAndRecordFraudEvent({
program: { id: link.programId },
partner: pick(webhookPartner, ["id", "email", "name"]),
customer: pick(customer, ["id", "email", "name"]),
commission: { id: createdCommission.commission?.id },
link: pick(link, ["id"]),
click: pick(clickData, ["url", "referer"]),
event: { id: leadEventId },
}),
]);
}

Expand Down
38 changes: 20 additions & 18 deletions apps/web/lib/api/conversions/track-sale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,15 +378,16 @@ const _trackLead = async ({
eventType: "lead",
}),

detectAndRecordFraudEvent({
program: { id: link.programId },
partner: pick(webhookPartner, ["id", "email", "name"]),
customer: pick(customer, ["id", "email", "name"]),
commission: { id: commission?.id },
link: pick(link, ["id"]),
click: pick(leadEventData, ["url", "referer"]),
event: { id: leadEventData.event_id },
}),
webhookPartner &&
detectAndRecordFraudEvent({
program: { id: link.programId },
partner: pick(webhookPartner, ["id", "email", "name"]),
customer: pick(customer, ["id", "email", "name"]),
commission: { id: commission?.id },
link: pick(link, ["id"]),
click: pick(leadEventData, ["url", "referer"]),
event: { id: leadEventData.event_id },
}),
]);
}

Expand Down Expand Up @@ -589,15 +590,16 @@ const _trackSale = async ({
eventType: "sale",
}),

detectAndRecordFraudEvent({
program: { id: link.programId },
partner: pick(webhookPartner, ["id", "email", "name"]),
customer: pick(customer, ["id", "email", "name"]),
commission: { id: createdCommission.commission?.id },
link: pick(link, ["id"]),
click: pick(saleData, ["url", "referer"]),
event: { id: saleData.event_id },
}),
webhookPartner &&
detectAndRecordFraudEvent({
program: { id: link.programId },
partner: pick(webhookPartner, ["id", "email", "name"]),
customer: pick(customer, ["id", "email", "name"]),
commission: { id: createdCommission.commission?.id },
link: pick(link, ["id"]),
click: pick(saleData, ["url", "referer"]),
event: { id: saleData.event_id },
}),
]);
}

Expand Down
7 changes: 3 additions & 4 deletions apps/web/lib/api/fraud/detect-record-fraud-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export async function detectAndRecordFraudEvent(context: FraudEventContext) {
const result = fraudEventContext.safeParse(context);

if (!result.success) {
console.error(
`[detectAndRecordFraudEvent] Invalid context ${result.error}`,
);
return;
}

Expand Down Expand Up @@ -85,10 +88,6 @@ export async function detectAndRecordFraudEvent(context: FraudEventContext) {
return;
}

console.log(
`[detectAndRecordFraudEvents] fraud events detected ${JSON.stringify(newEvents, null, 2)}`,
);

await prisma.fraudEvent.createMany({
data: newEvents.map((event) => ({
id: createId({ prefix: "fre_" }),
Expand Down
19 changes: 2 additions & 17 deletions apps/web/lib/api/fraud/get-grouped-fraud-events.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
fraudEventSchema,
groupedFraudEventSchema,
groupedFraudEventsQuerySchema,
} from "@/lib/zod/schemas/fraud";
import { prisma } from "@dub/prisma";
Expand All @@ -24,9 +24,6 @@ interface QueryResult {
partnerName: string | null;
partnerEmail: string | null;
partnerImage: string | null;
customerId: string | null;
customerEmail: string | null;
customerName: string | null;
userId: string | null;
userName: string | null;
userImage: string | null;
Expand Down Expand Up @@ -76,15 +73,12 @@ export async function getGroupedFraudEvents({
fe.groupKey,
fe.commissionId,
fe.partnerId,
fe.customerId,
fe.userId,
dfe.lastOccurrenceAt,
dfe.eventCount,
p.name AS partnerName,
p.email AS partnerEmail,
p.image AS partnerImage,
c.email AS customerEmail,
c.name AS customerName,
u.name AS userName,
u.image AS userImage
FROM (
Expand All @@ -101,8 +95,6 @@ export async function getGroupedFraudEvents({
ON fe.id = dfe.latestEventId
LEFT JOIN Partner p
ON p.id = fe.partnerId
LEFT JOIN Customer c
ON c.id = fe.customerId
LEFT JOIN User u
ON u.id = fe.userId
${orderByClause}
Expand All @@ -126,13 +118,6 @@ export async function getGroupedFraudEvents({
image: event.partnerImage,
}
: null,
customer: event.customerId
? {
id: event.customerId,
name: event.customerName,
email: event.customerEmail,
}
: null,
user: event.userId
? {
id: event.userId,
Expand All @@ -142,5 +127,5 @@ export async function getGroupedFraudEvents({
: null,
}));

return z.array(fraudEventSchema).parse(groupedEvents);
return z.array(groupedFraudEventSchema).parse(groupedEvents);
}
4 changes: 2 additions & 2 deletions apps/web/lib/middleware/utils/get-identity-hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { DUB_TEST_IDENTITY_HEADER } from "tests/utils/resource";
export async function getIdentityHash(req: Request) {
// If provided, use this identity directly (for E2E)
if (
process.env.NODE_ENV === "test" ||
process.env.NODE_ENV === "development"
process.env.NODE_ENV === "development" ||
process.env.VERCEL_ENV === "preview"
) {
const testOverride = req.headers.get(DUB_TEST_IDENTITY_HEADER);

Expand Down
4 changes: 2 additions & 2 deletions apps/web/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ import { DiscountCodeSchema, DiscountSchema } from "./zod/schemas/discount";
import { EmailDomainSchema } from "./zod/schemas/email-domains";
import { FolderSchema } from "./zod/schemas/folders";
import {
fraudEventSchema,
groupedFraudEventSchema,
fraudRuleSchema,
updateFraudRuleSettingsSchema,
} from "./zod/schemas/fraud";
Expand Down Expand Up @@ -673,7 +673,7 @@ export type WorkflowAttribute = (typeof WORKFLOW_ATTRIBUTES)[number];

export type EmailDomainProps = z.infer<typeof EmailDomainSchema>;

export type fraudEventGroupProps = z.infer<typeof fraudEventSchema>;
export type fraudEventGroupProps = z.infer<typeof groupedFraudEventSchema>;

export type ExtendedFraudRuleType =
| FraudRuleType
Expand Down
7 changes: 1 addition & 6 deletions apps/web/lib/zod/schemas/fraud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { UserSchema } from "./users";

export const MAX_RESOLUTION_REASON_LENGTH = 200;

export const fraudEventSchema = z.object({
export const groupedFraudEventSchema = z.object({
id: z.string(),
type: z.nativeEnum(FraudRuleType),
status: z.nativeEnum(FraudEventStatus),
Expand All @@ -24,11 +24,6 @@ export const fraudEventSchema = z.object({
email: true,
image: true,
}),
customer: CustomerSchema.pick({
id: true,
name: true,
email: true,
}).nullable(),
user: UserSchema.pick({
id: true,
name: true,
Expand Down
67 changes: 42 additions & 25 deletions apps/web/tests/fraud/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Customer, fraudEventGroupProps, TrackLeadResponse } from "@/lib/types";
import { FraudRuleType } from "@prisma/client";
import { randomCustomer, randomId } from "tests/utils/helpers";
import { randomCustomer, randomId, retry } from "tests/utils/helpers";
import { HttpClient } from "tests/utils/http";
import {
DUB_TEST_IDENTITY_HEADER,
Expand All @@ -11,7 +11,7 @@ import {
import { describe, expect, test } from "vitest";
import { IntegrationHarness } from "../utils/integration";

describe.skip.concurrent("/fraud/**", async () => {
describe.concurrent("/fraud/**", async () => {
const h = new IntegrationHarness();
const { http } = await h.init();

Expand Down Expand Up @@ -187,12 +187,9 @@ const verifyFraudEvent = async ({
ruleType,
}: {
http: HttpClient;
customer: Pick<Customer, "externalId" | "name" | "email">;
customer: Pick<Customer, "externalId">;
ruleType: FraudRuleType;
}) => {
// Wait for 8 seconds to reduce flakiness
await new Promise((resolve) => setTimeout(resolve, 8000));

// Resolve customerId from customerExternalID
const { data: customers } = await http.get<Customer[]>({
path: "/customers",
Expand All @@ -201,22 +198,15 @@ const verifyFraudEvent = async ({

expect(customers.length).toBeGreaterThan(0);

const customerFound = customers[0];

// Fetch fraud events for the current customer
const { data: fraudEvents } = await http.get<fraudEventGroupProps[]>({
path: "/fraud/events",
query: {
partnerId: E2E_FRAUD_PARTNER.id,
type: ruleType,
customerId: customerFound.id,
},
// Wait until fraud event is available
const fraudEvent = await waitForFraudEvent({
http,
partnerId: E2E_FRAUD_PARTNER.id,
customerId: customers[0].id,
ruleType,
});

expect(fraudEvents.length).toBeGreaterThan(0);

const fraudEvent = fraudEvents[0];

// Assert fraud event shape
expect(fraudEvent).toStrictEqual({
id: expect.any(String),
type: ruleType,
Expand All @@ -232,11 +222,38 @@ const verifyFraudEvent = async ({
email: expect.any(String),
image: null,
},
customer: {
id: customerFound.id,
name: expect.any(String),
email: expect.any(String),
},
user: null,
});
};

async function waitForFraudEvent({
http,
partnerId,
customerId,
ruleType,
}: {
http: HttpClient;
partnerId: string;
customerId: string;
ruleType: FraudRuleType;
}) {
return await retry(
async () => {
const { data } = await http.get<fraudEventGroupProps[]>({
path: "/fraud/events",
query: {
partnerId,
type: ruleType,
customerId,
},
});

if (!data.length) {
throw new Error("Fraud event not ready.");
}

return data[0];
},
{ retries: 10, interval: 300 },
);
}
23 changes: 23 additions & 0 deletions apps/web/tests/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,26 @@ export const randomEmail = ({
export const randomSaleAmount = () => {
return randomValue([400, 900, 1900]);
};

export async function retry<T>(
fn: () => Promise<T>,
{
retries = 10,
interval = 300,
}: { retries?: number; interval?: number } = {},
): Promise<T> {
let lastError;

for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (err) {
lastError = err;
if (i < retries - 1) {
await new Promise((res) => setTimeout(res, interval));
}
}
}

throw lastError;
}