Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -14,7 +14,7 @@ const inputSchema = z.object({
provider: z.enum(DiscountProvider),
});

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

Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/(ee)/api/cron/partners/ban/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export const POST = withCron(async ({ rawBody }) => {
recordLink(links, { deleted: true }),

// Queue discount code deletions
deleteDiscountCodes(discountCodes),
deleteDiscountCodes(discountCodes, { isSoftDelete: true }),
]);

// Send email
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/(ee)/api/cron/partners/deactivate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const POST = withCron(async ({ rawBody }) => {
const discountCodes = programEnrollments.flatMap(({ discountCodes }) =>
discountCodes.map((dc) => dc),
);
await deleteDiscountCodes(discountCodes);
await deleteDiscountCodes(discountCodes, { isSoftDelete: false });
console.log("[bulkDeactivatePartners] Queued discount code deletions.");

// Find the program
Expand Down
7 changes: 5 additions & 2 deletions apps/web/app/(ee)/api/discount-codes/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,15 @@ export const POST = withWorkspace(
code,
},
},
include: {
partner: true,
},
});

if (duplicateByCode) {
throw new DubApiError({
code: "bad_request",
message: `A discount with the code ${code} already exists in the program. Please choose a different code.`,
code: "conflict",
message: `This discount code "${code}" is already in use by [${duplicateByCode.partner.email}](https://app.dub.co/${workspace.slug}/program/partners/${duplicateByCode.partner.id}). Please choose a different code.`,
});
}
Comment thread
steven-tey marked this conversation as resolved.
}
Expand Down
25 changes: 14 additions & 11 deletions apps/web/lib/discounts/delete-discount-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type DeleteDiscountCodesParams = Pick<
// 4. When a partner is moved to a different group
export async function deleteDiscountCodes(
input: (DeleteDiscountCodesParams | null | undefined)[],
{ isSoftDelete = false }: { isSoftDelete?: boolean } = {},
) {
const discountCodes = input.filter(
(dc): dc is NonNullable<typeof dc> => dc != null,
Expand All @@ -29,18 +30,20 @@ export async function deleteDiscountCodes(
return;
}

// Delete the discount codes from the database
const deletedDiscountCodes = await prisma.discountCode.deleteMany({
where: {
id: {
in: discountCodes.map(({ id }) => id),
if (!isSoftDelete) {
// Delete the discount codes from the database
const deletedDiscountCodes = await prisma.discountCode.deleteMany({
where: {
id: {
in: discountCodes.map(({ id }) => id),
},
},
},
});
});

console.log(
`[deleteDiscountCodes] Deleted ${deletedDiscountCodes.count} discount codes.`,
);
console.log(
`[deleteDiscountCodes] Deleted ${deletedDiscountCodes.count} discount codes.`,
);
}

// Only enqueue external-provider cleanup for codes whose provider is known.
// Orphaned codes (discount relation is null) still get deleted locally above
Expand All @@ -63,7 +66,7 @@ export async function deleteDiscountCodes(
for (const chunkOfCodes of chunks) {
await enqueueBatchJobs(
chunkOfCodes.map((discountCode) => ({
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/discount-codes/delete`,
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/discount-codes/disable`,
method: "POST",
queueName: "delete-discount-code",
body: {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@
"remark-gfm": "^4.0.0",
"sanitize-html": "^2.17.0",
"shiki": "^1.14.1",
"sonner": "^1.4.41",
"sonner": "^2.0.7",
"streamdown": "^2.3.0",
"stripe": "^18.2.0",
"svix": "^1.76.1",
Expand Down
43 changes: 23 additions & 20 deletions apps/web/ui/modals/add-discount-code-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import { toast } from "sonner";
import { useDebounce } from "use-debounce";
import * as z from "zod/v4";
import { ERROR_MAP } from "../partners/constants";
import { X } from "../shared/icons";
import { CustomToast } from "../shared/custom-toast";
import { AlertCircleFill, X } from "../shared/icons";
Comment thread
steven-tey marked this conversation as resolved.
import { UpgradeRequiredToast } from "../shared/upgrade-required-toast";

type FormData = z.infer<typeof createDiscountCodeSchema>;
Expand Down Expand Up @@ -87,28 +88,30 @@ const AddDiscountCodeModal = ({
toast.success("Discount code created and copied to clipboard!");
},
onError: (error) => {
if (error) {
const code = Object.keys(ERROR_MAP).find((key) =>
error.startsWith(key),
);
const code = Object.keys(ERROR_MAP).find((key) =>
error.startsWith(key),
);

if (code) {
const { title, ctaLabel, ctaUrl } = ERROR_MAP[code];
const message = error.replace(`${code}: `, "");
if (code) {
const { title, ctaLabel, ctaUrl } = ERROR_MAP[code];
const message = error.replace(`${code}: `, "");

toast.custom(() => (
<UpgradeRequiredToast
title={title}
message={message}
ctaLabel={ctaLabel}
ctaUrl={ctaUrl}
/>
));
return;
}
toast.custom(() => (
<UpgradeRequiredToast
title={title}
message={message}
ctaLabel={ctaLabel}
ctaUrl={ctaUrl}
/>
));
return;
} else if (error.includes("already in use")) {
toast.custom(() => (
<CustomToast icon={AlertCircleFill}>{error}</CustomToast>
));
} else {
toast.error(error);
}

toast.error(error);
},
});
};
Expand Down
17 changes: 3 additions & 14 deletions apps/web/ui/shared/custom-toast.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import ReactMarkdown from "react-markdown";
import { MarkdownDescription } from "./markdown-description";

export function CustomToast({
icon: Icon,
Expand All @@ -10,20 +10,9 @@ export function CustomToast({
return (
<div className="flex items-center gap-1.5 rounded-lg bg-white p-4 text-sm shadow-[0_4px_12px_#0000001a]">
{Icon && <Icon className="size-[18px] shrink-0 text-black" />}
<ReactMarkdown
className="text-[13px] font-medium text-neutral-900"
components={{
a: ({ node, ...props }) => (
<a
{...props}
target="_blank"
className="text-neutral-500 underline transition-colors hover:text-neutral-800"
/>
),
}}
>
<MarkdownDescription className="text-[13px] font-medium text-neutral-900">
{children}
</ReactMarkdown>
</MarkdownDescription>
</div>
);
}
39 changes: 25 additions & 14 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading