Skip to content

Commit 7d45f87

Browse files
authored
Merge branch 'main' into main
2 parents 08d73a5 + 525390a commit 7d45f87

File tree

64 files changed

+3113
-747
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+3113
-747
lines changed

apps/web/app/(ee)/api/cron/cleanup/e2e-tests/route.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { deleteDiscountCodes } from "@/lib/api/discounts/delete-discount-code";
21
import { markDomainAsDeleted } from "@/lib/api/domains/mark-domain-deleted";
32
import { handleAndReturnErrorResponse } from "@/lib/api/errors";
43
import { bulkDeleteLinks } from "@/lib/api/links/bulk-delete-links";
@@ -100,12 +99,20 @@ export async function POST(req: Request) {
10099

101100
// Delete the links
102101
if (links.length > 0) {
103-
await deleteDiscountCodes(links.flatMap((link) => link.discountCode));
102+
const linkIds = links.map((link) => link.id);
103+
104+
await prisma.discountCode.deleteMany({
105+
where: {
106+
linkId: {
107+
in: linkIds,
108+
},
109+
},
110+
});
104111

105112
await prisma.link.deleteMany({
106113
where: {
107114
id: {
108-
in: links.map((link) => link.id),
115+
in: linkIds,
109116
},
110117
},
111118
});

apps/web/app/(ee)/api/groups/[groupIdOrSlug]/default-links/[defaultLinkId]/route.ts

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { getDomainOrThrow } from "@/lib/api/domains/get-domain-or-throw";
21
import { queueDomainUpdate } from "@/lib/api/domains/queue-domain-update";
32
import { DubApiError } from "@/lib/api/errors";
43
import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw";
@@ -26,41 +25,34 @@ export const PATCH = withWorkspace(
2625
await parseRequestBody(req),
2726
);
2827

29-
const [group, domainRecord] = await Promise.all([
30-
prisma.partnerGroup.findUniqueOrThrow({
31-
where: {
32-
...(groupIdOrSlug.startsWith("grp_")
33-
? {
34-
id: groupIdOrSlug,
35-
}
36-
: {
37-
programId_slug: {
38-
programId,
39-
slug: groupIdOrSlug,
40-
},
41-
}),
42-
programId,
43-
},
44-
include: {
45-
utmTemplate: true,
46-
partnerGroupDefaultLinks: {
47-
where: {
48-
id: params.defaultLinkId,
49-
},
28+
const group = await prisma.partnerGroup.findUniqueOrThrow({
29+
where: {
30+
...(groupIdOrSlug.startsWith("grp_")
31+
? {
32+
id: groupIdOrSlug,
33+
}
34+
: {
35+
programId_slug: {
36+
programId,
37+
slug: groupIdOrSlug,
38+
},
39+
}),
40+
programId,
41+
},
42+
include: {
43+
utmTemplate: true,
44+
partnerGroupDefaultLinks: {
45+
where: {
46+
id: params.defaultLinkId,
5047
},
51-
program: {
52-
select: {
53-
domain: true,
54-
},
48+
},
49+
program: {
50+
select: {
51+
domain: true,
5552
},
5653
},
57-
}),
58-
59-
getDomainOrThrow({
60-
workspace,
61-
domain,
62-
}),
63-
]);
54+
},
55+
});
6456

6557
if (group.partnerGroupDefaultLinks.length === 0) {
6658
throw new DubApiError({
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { DubApiError } from "@/lib/api/errors";
2+
import { getProgramEnrollmentOrThrow } from "@/lib/api/programs/get-program-enrollment-or-throw";
3+
import { withPartnerProfile } from "@/lib/auth/partner";
4+
import {
5+
activityLogSchema,
6+
getActivityLogsQuerySchema,
7+
} from "@/lib/zod/schemas/activity-log";
8+
import { prisma } from "@dub/prisma";
9+
import { NextResponse } from "next/server";
10+
import * as z from "zod/v4";
11+
12+
export const GET = withPartnerProfile(
13+
async ({ partner, params, searchParams }) => {
14+
const { resourceType, resourceId, action } =
15+
getActivityLogsQuerySchema.parse(searchParams);
16+
17+
// Limit to referral for now
18+
if (resourceType !== "referral") {
19+
throw new DubApiError({
20+
code: "bad_request",
21+
message: "Resource type must be referral.",
22+
});
23+
}
24+
25+
const programEnrollment = await getProgramEnrollmentOrThrow({
26+
partnerId: partner.id,
27+
programId: params.programId,
28+
include: {},
29+
});
30+
31+
// Check if the resource is a referral and belongs to the program and partner
32+
if (resourceType === "referral") {
33+
const referral = await prisma.partnerReferral.findUnique({
34+
where: {
35+
id: resourceId,
36+
programId: programEnrollment.programId,
37+
partnerId: partner.id,
38+
},
39+
select: {
40+
id: true,
41+
},
42+
});
43+
44+
if (!referral) {
45+
throw new DubApiError({
46+
code: "not_found",
47+
message: "Referral not found.",
48+
});
49+
}
50+
}
51+
52+
const activityLogs = await prisma.activityLog.findMany({
53+
where: {
54+
programId: programEnrollment.programId,
55+
resourceType,
56+
resourceId,
57+
action,
58+
},
59+
orderBy: {
60+
createdAt: "desc",
61+
},
62+
take: 100,
63+
});
64+
65+
return NextResponse.json(z.array(activityLogSchema).parse(activityLogs));
66+
},
67+
);

apps/web/app/(ee)/api/programs/[programId]/payouts/count/route.ts renamed to apps/web/app/(ee)/api/payouts/count/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import { prisma } from "@dub/prisma";
77
import { FraudEventStatus, PayoutStatus, Prisma } from "@dub/prisma/client";
88
import { NextResponse } from "next/server";
99

10-
// GET /api/programs/[programId]/payouts/count
10+
// GET /api/payouts/count
1111
export const GET = withWorkspace(async ({ workspace, searchParams }) => {
1212
const programId = getDefaultProgramIdOrThrow(workspace);
1313

1414
const isHoldStatus = searchParams.status === "hold";
1515
const { status: _status, ...restSearchParams } = searchParams;
16+
1617
let { status, partnerId, groupBy, eligibility, invoiceId } =
1718
payoutsCountQuerySchema.parse(
1819
isHoldStatus ? restSearchParams : searchParams,

apps/web/app/(ee)/api/programs/[programId]/payouts/route.ts renamed to apps/web/app/(ee)/api/payouts/route.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { DubApiError } from "@/lib/api/errors";
12
import { getEffectivePayoutMode } from "@/lib/api/payouts/get-effective-payout-mode";
23
import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw";
34
import { getProgramOrThrow } from "@/lib/api/programs/get-program-or-throw";
@@ -11,20 +12,51 @@ import { FraudEventStatus, PayoutStatus } from "@dub/prisma/client";
1112
import { NextResponse } from "next/server";
1213
import * as z from "zod/v4";
1314

14-
// GET /api/programs/[programId]/payouts - get all payouts for a program
15+
// GET /api/payouts - get all payouts for a program
1516
export const GET = withWorkspace(async ({ workspace, searchParams }) => {
1617
const programId = getDefaultProgramIdOrThrow(workspace);
1718

1819
const isHoldStatus = searchParams.status === "hold";
1920
const { status: _status, ...restSearchParams } = searchParams;
2021

21-
let { status, partnerId, invoiceId, sortBy, sortOrder, page, pageSize } =
22-
payoutsQuerySchema.parse(isHoldStatus ? restSearchParams : searchParams);
22+
let {
23+
status,
24+
partnerId,
25+
tenantId,
26+
invoiceId,
27+
sortBy,
28+
sortOrder,
29+
page,
30+
pageSize,
31+
} = payoutsQuerySchema.parse(isHoldStatus ? restSearchParams : searchParams);
2332

2433
if (isHoldStatus) {
2534
status = PayoutStatus.pending;
2635
}
2736

37+
if (tenantId && !partnerId) {
38+
const programEnrollment = await prisma.programEnrollment.findUnique({
39+
where: {
40+
tenantId_programId: {
41+
tenantId,
42+
programId,
43+
},
44+
},
45+
select: {
46+
partnerId: true,
47+
},
48+
});
49+
50+
if (!programEnrollment) {
51+
throw new DubApiError({
52+
code: "not_found",
53+
message: `Partner with specified tenantId ${tenantId} not found.`,
54+
});
55+
}
56+
57+
partnerId = programEnrollment.partnerId;
58+
}
59+
2860
const program = await getProgramOrThrow({
2961
workspaceId: workspace.id,
3062
programId,
@@ -49,6 +81,7 @@ export const GET = withWorkspace(async ({ workspace, searchParams }) => {
4981
include: {
5082
programEnrollment: true,
5183
partner: true,
84+
user: true,
5285
},
5386
skip: (page - 1) * pageSize,
5487
take: pageSize,

apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/overview/page-client.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export function PageClient() {
5353
return true;
5454
}, [data]);
5555

56-
const reward: Omit<RewardProps, "id"> = {
56+
const reward: Omit<RewardProps, "id" | "updatedAt"> = {
5757
type: (data.type ?? "flat") as RewardStructure,
5858
amountInCents: data.amountInCents != null ? data.amountInCents * 100 : null,
5959
amountInPercentage: data.amountInPercentage,

apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/rewards/form.tsx

Lines changed: 47 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -315,61 +315,59 @@ export function Form() {
315315

316316
{defaultRewardType === "sale" && (
317317
<div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
318-
{PAYOUT_MODELS.map(
319-
({ key, label, description, mostCommon }) => {
320-
const isSelected = key === type;
318+
{PAYOUT_MODELS.map(({ key, label, description, mostCommon }) => {
319+
const isSelected = key === type;
321320

322-
return (
323-
<div
324-
key={key}
321+
return (
322+
<div
323+
key={key}
324+
className={cn(
325+
"flex flex-col items-center",
326+
mostCommon &&
327+
"rounded-md border border-neutral-200 bg-neutral-100",
328+
)}
329+
>
330+
<label
325331
className={cn(
326-
"flex flex-col items-center",
327-
mostCommon &&
328-
"rounded-md border border-neutral-200 bg-neutral-100",
332+
"relative flex w-full cursor-pointer items-start gap-0.5 rounded-md border border-neutral-200 bg-white p-3 text-neutral-600 hover:bg-neutral-50",
333+
"transition-all duration-150",
334+
mostCommon && "border-transparent shadow-sm",
335+
isSelected &&
336+
"border-black bg-neutral-50 text-neutral-900 ring-1 ring-black",
329337
)}
330338
>
331-
<label
339+
<input
340+
type="radio"
341+
value={key}
342+
className="hidden"
343+
checked={isSelected}
344+
onChange={() =>
345+
setValue("type", key, { shouldDirty: true })
346+
}
347+
/>
348+
<div className="flex grow flex-col text-sm">
349+
<span className="text-sm font-semibold text-neutral-900">
350+
{label}
351+
</span>
352+
<span className="text-sm font-normal text-neutral-600">
353+
{description}
354+
</span>
355+
</div>
356+
<CircleCheckFill
332357
className={cn(
333-
"relative flex w-full cursor-pointer items-start gap-0.5 rounded-md border border-neutral-200 bg-white p-3 text-neutral-600 hover:bg-neutral-50",
334-
"transition-all duration-150",
335-
mostCommon && "border-transparent shadow-sm",
336-
isSelected &&
337-
"border-black bg-neutral-50 text-neutral-900 ring-1 ring-black",
358+
"-mr-px -mt-px flex size-4 scale-75 items-center justify-center rounded-full opacity-0 transition-[transform,opacity] duration-150",
359+
isSelected && "scale-100 opacity-100",
338360
)}
339-
>
340-
<input
341-
type="radio"
342-
value={key}
343-
className="hidden"
344-
checked={isSelected}
345-
onChange={() =>
346-
setValue("type", key, { shouldDirty: true })
347-
}
348-
/>
349-
<div className="flex grow flex-col text-sm">
350-
<span className="text-sm font-semibold text-neutral-900">
351-
{label}
352-
</span>
353-
<span className="text-sm font-normal text-neutral-600">
354-
{description}
355-
</span>
356-
</div>
357-
<CircleCheckFill
358-
className={cn(
359-
"-mr-px -mt-px flex size-4 scale-75 items-center justify-center rounded-full opacity-0 transition-[transform,opacity] duration-150",
360-
isSelected && "scale-100 opacity-100",
361-
)}
362-
/>
363-
</label>
364-
{mostCommon && (
365-
<span className="py-0.5 text-xs font-medium text-neutral-500">
366-
Most common
367-
</span>
368-
)}
369-
</div>
370-
);
371-
},
372-
)}
361+
/>
362+
</label>
363+
{mostCommon && (
364+
<span className="py-0.5 text-xs font-medium text-neutral-500">
365+
Most common
366+
</span>
367+
)}
368+
</div>
369+
);
370+
})}
373371
</div>
374372
)}
375373

0 commit comments

Comments
 (0)