Skip to content

Commit 6e44c5b

Browse files
authored
Merge pull request #4112 from dubinc/soft-delete-codes
Add `isSoftDelete` prop to `deleteDiscountCodes`
2 parents 4a6d2a5 + 15b0c6f commit 6e44c5b

17 files changed

Lines changed: 182 additions & 88 deletions

File tree

apps/web/app/(ee)/api/cron/discount-codes/delete/route.ts renamed to apps/web/app/(ee)/api/cron/discount-codes/disable/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const inputSchema = z.object({
1414
provider: z.enum(DiscountProvider),
1515
});
1616

17-
// POST /api/cron/discount-codes/delete
17+
// POST /api/cron/discount-codes/disable – disable a discount code from the provider (Stripe, Shopify, etc.)
1818
export const POST = withCron(async ({ rawBody }) => {
1919
const { provider, code, programId } = inputSchema.parse(JSON.parse(rawBody));
2020

apps/web/app/(ee)/api/cron/partners/ban/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export const POST = withCron(async ({ rawBody }) => {
138138
recordLink(links, { deleted: true }),
139139

140140
// Queue discount code deletions
141-
deleteDiscountCodes(discountCodes),
141+
deleteDiscountCodes(discountCodes, { isSoftDelete: true }),
142142
]);
143143

144144
// Send email

apps/web/app/(ee)/api/cron/partners/deactivate/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export const POST = withCron(async ({ rawBody }) => {
5858
const discountCodes = programEnrollments.flatMap(({ discountCodes }) =>
5959
discountCodes.map((dc) => dc),
6060
);
61-
await deleteDiscountCodes(discountCodes);
61+
await deleteDiscountCodes(discountCodes, { isSoftDelete: true });
6262
console.log("[bulkDeactivatePartners] Queued discount code deletions.");
6363

6464
// Find the program

apps/web/app/(ee)/api/discount-codes/route.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
DiscountCodeSchema,
1212
getDiscountCodesQuerySchema,
1313
} from "@/lib/zod/schemas/discount";
14+
import { APP_DOMAIN } from "@dub/utils";
1415
import { waitUntil } from "@vercel/functions";
1516
import { NextResponse } from "next/server";
1617

@@ -100,12 +101,15 @@ export const POST = withWorkspace(
100101
code,
101102
},
102103
},
104+
include: {
105+
partner: true,
106+
},
103107
});
104108

105109
if (duplicateByCode) {
106110
throw new DubApiError({
107-
code: "bad_request",
108-
message: `A discount with the code ${code} already exists in the program. Please choose a different code.`,
111+
code: "conflict",
112+
message: `This discount code "${code}" is already in use by [${duplicateByCode.partner.email}](${APP_DOMAIN}/${workspace.slug}/program/partners/${duplicateByCode.partner.id}). Please choose a different code.`,
109113
});
110114
}
111115
}

apps/web/app/(ee)/api/partner-profile/programs/[programId]/links/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export const GET = withPartnerProfile(async ({ partner, params }) => {
3737
return {
3838
...link,
3939
discountCode: discountCode?.code,
40+
discountCodeDisabledAt: discountCode?.disabledAt ?? null,
4041
};
4142
});
4243

apps/web/app/(ee)/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,21 @@ export async function attributeViaPromotionCodeId({
7070
code: promotionCode.code,
7171
},
7272
},
73-
select: {
73+
include: {
7474
link: true,
7575
},
7676
});
7777

7878
if (!discountCode) {
7979
console.log(
80-
`Couldn't find link associated with promotion code ${promotionCode.code}, skipping...`,
80+
`Couldn't find discount code "${promotionCode.code}" in program "${workspace.defaultProgramId}", skipping...`,
81+
);
82+
return null;
83+
}
84+
85+
if (discountCode.disabledAt) {
86+
console.log(
87+
`Discount code "${discountCode.code}" is disabled, skipping...`,
8188
);
8289
return null;
8390
}

apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/links/partner-link-card.tsx

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
getApexDomain,
2929
getPrettyUrl,
3030
nFormatter,
31+
PARTNERS_DOMAIN,
3132
} from "@dub/utils";
3233
import NumberFlow from "@number-flow/react";
3334
import Link from "next/link";
@@ -75,6 +76,19 @@ export function PartnerLinkCard({ link }: { link: PartnerProfileLinkProps }) {
7576

7677
const isDeactivated = programEnrollment?.status === "deactivated";
7778

79+
const discountCodeSection = link.discountCode ? (
80+
<div className="hidden items-center gap-1.5 rounded-xl border border-neutral-200 py-1 pl-2 pr-1 sm:flex">
81+
<span className="text-sm leading-none text-neutral-500">
82+
Discount code
83+
</span>
84+
<DiscountCodeBadge
85+
code={link.discountCode}
86+
disabledAt={link.discountCodeDisabledAt}
87+
disabledTooltip={`This discount code was disabled by the program. [Contact the program owner](${PARTNERS_DOMAIN}/messages/${programEnrollment?.program.slug}) if you need a new code.`}
88+
/>
89+
</div>
90+
) : null;
91+
7892
return (
7993
<CardList.Card
8094
innerClassName={cn("px-0 py-0 group/card", isDeactivated && "opacity-80")}
@@ -153,20 +167,14 @@ export function PartnerLinkCard({ link }: { link: PartnerProfileLinkProps }) {
153167
</StatusBadge>
154168
);
155169
})()}
156-
{link.discountCode && (
157-
<Tooltip
158-
content={
159-
"This program supports discount code tracking. Copy the code to use it in podcasts, videos, etc. [Learn more](https://dub.co/help/article/dual-sided-incentives)"
160-
}
161-
>
162-
<div className="hidden items-center gap-1.5 rounded-xl border border-neutral-200 py-1 pl-2 pr-1 sm:flex">
163-
<span className="text-sm leading-none text-neutral-500">
164-
Discount code
165-
</span>
166-
<DiscountCodeBadge code={link.discountCode} />
167-
</div>
168-
</Tooltip>
169-
)}
170+
{discountCodeSection &&
171+
(link.discountCodeDisabledAt ? (
172+
discountCodeSection
173+
) : (
174+
<Tooltip content="This program supports discount code tracking. Copy the code to use it in podcasts, videos, etc. [Learn more](https://dub.co/help/article/dual-sided-incentives)">
175+
{discountCodeSection}
176+
</Tooltip>
177+
))}
170178
{displayOption === "cards" && <StatsBadge link={link} />}
171179
<Controls link={link} />
172180
</div>

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/links/page.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,12 @@ const PartnerDiscountCodes = ({
333333
{
334334
id: "code",
335335
header: "Code",
336-
cell: ({ row }) => <DiscountCodeBadge code={row.original.code} />,
336+
cell: ({ row }) => (
337+
<DiscountCodeBadge
338+
code={row.original.code}
339+
disabledAt={row.original.disabledAt}
340+
/>
341+
),
337342
},
338343
{
339344
id: "shortLink",

apps/web/lib/discounts/delete-discount-code.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type DeleteDiscountCodesParams = Pick<
1717
// 4. When a partner is moved to a different group
1818
export async function deleteDiscountCodes(
1919
input: (DeleteDiscountCodesParams | null | undefined)[],
20+
{ isSoftDelete = false }: { isSoftDelete?: boolean } = {},
2021
) {
2122
const discountCodes = input.filter(
2223
(dc): dc is NonNullable<typeof dc> => dc != null,
@@ -29,18 +30,36 @@ export async function deleteDiscountCodes(
2930
return;
3031
}
3132

32-
// Delete the discount codes from the database
33-
const deletedDiscountCodes = await prisma.discountCode.deleteMany({
34-
where: {
35-
id: {
36-
in: discountCodes.map(({ id }) => id),
33+
if (isSoftDelete) {
34+
// Soft delete the discount codes from the database (mark them as disabled)
35+
const disabledDiscountCodes = await prisma.discountCode.updateMany({
36+
where: {
37+
id: {
38+
in: discountCodes.map(({ id }) => id),
39+
},
40+
},
41+
data: {
42+
disabledAt: new Date(),
3743
},
38-
},
39-
});
44+
});
4045

41-
console.log(
42-
`[deleteDiscountCodes] Deleted ${deletedDiscountCodes.count} discount codes.`,
43-
);
46+
console.log(
47+
`[deleteDiscountCodes] Disabled ${disabledDiscountCodes.count} discount codes.`,
48+
);
49+
} else {
50+
// Delete the discount codes from the database
51+
const deletedDiscountCodes = await prisma.discountCode.deleteMany({
52+
where: {
53+
id: {
54+
in: discountCodes.map(({ id }) => id),
55+
},
56+
},
57+
});
58+
59+
console.log(
60+
`[deleteDiscountCodes] Deleted ${deletedDiscountCodes.count} discount codes.`,
61+
);
62+
}
4463

4564
// Only enqueue external-provider cleanup for codes whose provider is known.
4665
// Orphaned codes (discount relation is null) still get deleted locally above
@@ -63,7 +82,7 @@ export async function deleteDiscountCodes(
6382
for (const chunkOfCodes of chunks) {
6483
await enqueueBatchJobs(
6584
chunkOfCodes.map((discountCode) => ({
66-
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/discount-codes/delete`,
85+
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/discount-codes/disable`,
6786
method: "POST",
6887
queueName: "delete-discount-code",
6988
body: {

apps/web/lib/zod/schemas/discount.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ export const DiscountCodeSchema = z.object({
6262
discountId: z.string().nullable(),
6363
partnerId: z.string(),
6464
linkId: z.string(),
65+
disabledAt: z.coerce
66+
.date()
67+
.nullish()
68+
.describe(
69+
"When this discount code was disabled, which happens when a partner is banned or deactivated.",
70+
),
6571
});
6672

6773
export const createDiscountCodeSchema = z.object({

0 commit comments

Comments
 (0)