[web] sponsorships/request: add coupon flow in sponsorships request form (#1493)

(cherry picked from commit d24a7c0f60)
This commit is contained in:
Nitesh Seram 2025-06-10 16:09:09 +05:30 committed by Yangshun
parent b14990e04c
commit a81cf8a73b
16 changed files with 449 additions and 116 deletions

View File

@ -19,3 +19,23 @@ export function priceRoundToNearestNiceNumber(priceParam: number) {
// Round to nearest 1000.
return Math.ceil(price / 1000) * 1000;
}
export function getDiscountedPrice({
amountOff,
percentOff,
price,
}: Readonly<{
amountOff?: number | null;
percentOff?: number | null;
price: number;
}>) {
if (amountOff) {
return price - amountOff;
}
if (percentOff) {
return price - (price * percentOff) / 100;
}
return price;
}

View File

@ -0,0 +1,12 @@
export const SponsorsPromoCodeConfig: Record<
string,
{
code: string;
percentOff: number;
}
> = {
TRIAL25: {
code: 'TRIAL25',
percentOff: 25,
},
} as const;

View File

@ -19,6 +19,7 @@ import Text from '~/components/ui/Text';
import SponsorsAdvertiseRequestForm from '../request/SponsorsAdvertiseRequestForm';
import SponsorsAdvertiseRequestReadonly from '../request/SponsorsAdvertiseRequestReadonly';
import { SponsorsPromoCodeConfig } from '../SponsorsPromoCodeConfig';
import SponsorsAdminAdRequestStatus from './SponsorsAdminAdRequestStatus';
type Props = Readonly<{
@ -111,6 +112,9 @@ export default function SponsorsAdminAdRequestPage({ adRequestId }: Props) {
ads,
company,
emails: adRequest.emails,
promoCode: adRequest.promoCode
? SponsorsPromoCodeConfig[adRequest.promoCode]
: null,
}}
mode="edit"
requestId={adRequest.id}
@ -134,6 +138,9 @@ export default function SponsorsAdminAdRequestPage({ adRequestId }: Props) {
company,
createdAt: adRequest.createdAt,
emails: adRequest.emails,
promoCode: adRequest.promoCode
? SponsorsPromoCodeConfig[adRequest.promoCode]
: null,
review,
updatedAt: adRequest.updatedAt,
}}

View File

@ -22,6 +22,7 @@ import TextInput from '~/components/ui/TextInput';
import { useI18nRouter } from '~/next-i18nostic/src';
import { SponsorsPromoCodeConfig } from '../SponsorsPromoCodeConfig';
import SponsorsAdvertiseRequestForm from './SponsorsAdvertiseRequestForm';
import SponsorsAdvertiseRequestReadonly from './SponsorsAdvertiseRequestReadonly';
import type {
@ -144,6 +145,9 @@ export default function SponsorsAdvertiseRequestEditPage({ adRequest }: Props) {
ads,
company,
emails: adRequest.emails,
promoCode: adRequest.promoCode
? SponsorsPromoCodeConfig[adRequest.promoCode]
: null,
}}
mode={adRequest.status === 'PENDING' ? 'edit' : 'readonly'}
requestId={adRequest.id}
@ -176,6 +180,9 @@ export default function SponsorsAdvertiseRequestEditPage({ adRequest }: Props) {
company,
createdAt: adRequest.createdAt,
emails: adRequest.emails,
promoCode: adRequest.promoCode
? SponsorsPromoCodeConfig[adRequest.promoCode]
: null,
review: null,
updatedAt: adRequest.updatedAt,
}}

View File

@ -162,6 +162,7 @@ export default function SponsorsAdvertiseRequestForm({
company: formData.company!,
emails: formData.emails,
id: props.requestId!,
promoCode: formData.promoCode ? formData.promoCode : undefined,
},
{
onError: (error) => {
@ -204,6 +205,7 @@ export default function SponsorsAdvertiseRequestForm({
agreement,
company: formData.company!,
emails: formData.emails,
promoCode: formData.promoCode ? formData.promoCode : undefined,
},
{
onError: (error) => {
@ -306,8 +308,12 @@ export default function SponsorsAdvertiseRequestForm({
<SponsorsAdvertiseRequestFormAdsSection
ads={formData.ads}
mode={mode}
promoCode={formData.promoCode}
sessionId={formData.sessionId}
updateAds={(ads) => setFormData((prev) => ({ ...prev, ads }))}
updatePromoCode={(promoCode) =>
setFormData((prev) => ({ ...prev, promoCode }))
}
updateStepStatus={(status) =>
setStepsStatus((prev) => ({ ...prev, ads: status }))
}
@ -342,6 +348,7 @@ export default function SponsorsAdvertiseRequestForm({
ads: formData.ads,
company: formData.company!,
emails: formData.emails,
promoCode: formData.promoCode,
}}
isSubmitting={
adRequestCreateMutation.isLoading ||

View File

@ -6,11 +6,15 @@ import { urlAddHttpsIfMissing } from '~/lib/urlValidation';
import RelativeTimestamp from '~/components/common/datetime/RelativeTimestamp';
import InterviewsMarketingHeroBrowserWindowFrame from '~/components/interviews/marketing/embed/InterviewsMarketingHeroBrowserWindowFrame';
import { FormattedMessage, useIntl } from '~/components/intl';
import { getDiscountedPrice } from '~/components/purchase/PurchasePricingUtils';
import SponsorsAdFormatGlobalBanner from '~/components/sponsors/ads/SponsorsAdFormatGlobalBanner';
import SponsorsAdFormatInContent from '~/components/sponsors/ads/SponsorsAdFormatInContent';
import SponsorsAdFormatInContentBodyRenderer from '~/components/sponsors/ads/SponsorsAdFormatInContentBodyRenderer';
import SponsorsAdFormatSpotlight from '~/components/sponsors/ads/SponsorsAdFormatSpotlight';
import type { SponsorsAdFormatFormItem } from '~/components/sponsors/request/types';
import type {
SponsorsAdFormatFormItem,
SponsorsPromoCode,
} from '~/components/sponsors/request/types';
import type { AdvertiseRequestFormValues } from '~/components/sponsors/request/useSponsorsAdvertiseRequestFormData';
import {
SponsorAdFormatConfigs,
@ -59,8 +63,16 @@ export default function SponsorsAdvertiseRequestReadonly({
onEdit,
}: Props) {
const intl = useIntl();
const { ads, agreement, company, createdAt, emails, review, updatedAt } =
data;
const {
ads,
agreement,
company,
createdAt,
emails,
promoCode,
review,
updatedAt,
} = data;
const { address, signatoryName, signatoryTitle } = company!;
const addressString = [
[address.line1, address.line2].filter(Boolean).join(', '),
@ -72,8 +84,22 @@ export default function SponsorsAdvertiseRequestReadonly({
.filter(Boolean)
.join(', ');
const totalPrice = ads.reduce(
(acc, curr) =>
acc +
curr.weeks.length * SponsorAdFormatConfigs[curr.format].pricePerWeekUSD,
0,
);
const discountedPrice = promoCode
? getDiscountedPrice({
percentOff: promoCode.percentOff,
price: totalPrice,
})
: totalPrice;
return (
<div className="flex flex-col gap-10">
<div className="flex w-full flex-col gap-10">
<div className="flex flex-col gap-6">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex flex-col gap-1.5">
@ -188,7 +214,7 @@ export default function SponsorsAdvertiseRequestReadonly({
<div className="flex flex-col items-start">
<ul className="flex w-full flex-col gap-4">
{ads.map((ad) => (
<AdFormatCard key={ad.id} ad={ad} />
<AdFormatCard key={ad.id} ad={ad} promoCode={promoCode} />
))}
</ul>
<div className="mt-4 flex w-full justify-end gap-4">
@ -198,14 +224,7 @@ export default function SponsorsAdvertiseRequestReadonly({
description="Total price label"
id="0kDCAp"
values={{
total: ads.reduce(
(acc, curr) =>
acc +
curr.weeks.length *
SponsorAdFormatConfigs[curr.format]
.pricePerWeekUSD,
0,
),
total: discountedPrice,
}}
/>
</Text>
@ -327,7 +346,10 @@ export default function SponsorsAdvertiseRequestReadonly({
);
}
function AdFormatCard({ ad }: Readonly<{ ad: SponsorsAdFormatFormItem }>) {
function AdFormatCard({
ad,
promoCode,
}: Readonly<{ ad: SponsorsAdFormatFormItem; promoCode: SponsorsPromoCode }>) {
const intl = useIntl();
const adFormatData = useSponsorsAdFormatData();
@ -447,7 +469,12 @@ function AdFormatCard({ ad }: Readonly<{ ad: SponsorsAdFormatFormItem }>) {
<Text color="secondary" size="body2">
$
{ad.weeks.length *
SponsorAdFormatConfigs[ad.format].pricePerWeekUSD}
(promoCode
? getDiscountedPrice({
percentOff: promoCode.percentOff,
price: SponsorAdFormatConfigs[ad.format].pricePerWeekUSD,
})
: SponsorAdFormatConfigs[ad.format].pricePerWeekUSD)}
</Text>
</AdDetailRow>
</div>

View File

@ -14,6 +14,7 @@ import { v4 as uuidv4 } from 'uuid';
import ConfirmationDialog from '~/components/common/ConfirmationDialog';
import type { StepsTabItemStatus } from '~/components/common/StepsTabs';
import { FormattedMessage, useIntl } from '~/components/intl';
import { getDiscountedPrice } from '~/components/purchase/PurchasePricingUtils';
import { useSponsorsAdFormatData } from '~/components/sponsors/SponsorsAdFormatConfigs';
import Badge from '~/components/ui/Badge';
import Button from '~/components/ui/Button';
@ -31,18 +32,24 @@ import {
import { themeBackgroundElementEmphasizedStateColor_Hover } from '../../../ui/theme';
import { SponsorAdFormatConfigs } from '../../SponsorsAdFormatConfigs';
import type { SponsorsAdFormatFormItem } from '../types';
import type { SponsorsAdFormatFormItem, SponsorsPromoCode } from '../types';
import SponsorsAdvertiseRequestFormAdsSectionGlobalBanner from './formats/SponsorsAdvertiseRequestFormAdsSectionGlobalBanner';
import SponsorsAdvertiseRequestFormAdsSectionInContent from './formats/SponsorsAdvertiseRequestFormAdsSectionInContent';
import SponsorsAdvertiseRequestFormAdsSectionSpotlight from './formats/SponsorsAdvertiseRequestFormAdsSectionSpotlight';
import SponsorsAdvertiseRequestPromoCode from './SponsorsAdvertiseRequestPromoCode';
type Props = Readonly<{
ads: Array<SponsorsAdFormatFormItem>;
mode: 'create' | 'edit' | 'readonly';
onPrevious: () => void;
onSubmit: () => void;
promoCode: SponsorsPromoCode;
sessionId: string;
updateAds(ads: Array<SponsorsAdFormatFormItem>): void;
updatePromoCode: ({
code,
percentOff,
}: Readonly<{ code: string; percentOff: number }>) => void;
updateStepStatus(status: StepsTabItemStatus): void;
}>;
@ -51,8 +58,10 @@ export default function SponsorsAdvertiseRequestFormAdsSection({
mode,
onPrevious,
onSubmit,
promoCode,
sessionId,
updateAds,
updatePromoCode,
updateStepStatus,
}: Props) {
const intl = useIntl();
@ -152,11 +161,32 @@ export default function SponsorsAdvertiseRequestFormAdsSection({
/>
</Text>
<div className="flex items-center gap-2 max-md:order-4 max-md:-mb-0.5 max-md:-mr-1">
<Text size="body2" weight="bold">
$
{ad.weeks.length *
SponsorAdFormatConfigs[ad.format].pricePerWeekUSD}
</Text>
<div className="flex items-center gap-1.5">
{promoCode?.percentOff && (
<Text
className="line-through"
color="secondary"
size="body2">
$
{ad.weeks.length *
SponsorAdFormatConfigs[ad.format].pricePerWeekUSD}
</Text>
)}
<Text size="body2" weight="bold">
$
{ad.weeks.length *
(promoCode?.percentOff
? getDiscountedPrice({
percentOff: promoCode?.percentOff,
price:
SponsorAdFormatConfigs[ad.format]
.pricePerWeekUSD,
})
: SponsorAdFormatConfigs[ad.format]
.pricePerWeekUSD)}
</Text>
</div>
<div className="flex items-center">
{isReadonly ? (
<Button
@ -238,7 +268,15 @@ export default function SponsorsAdvertiseRequestFormAdsSection({
(acc, curr) =>
acc +
curr.weeks.length *
SponsorAdFormatConfigs[curr.format].pricePerWeekUSD,
(promoCode?.percentOff
? getDiscountedPrice({
percentOff: promoCode?.percentOff,
price:
SponsorAdFormatConfigs[curr.format]
.pricePerWeekUSD,
})
: SponsorAdFormatConfigs[curr.format]
.pricePerWeekUSD),
0,
),
}}
@ -553,20 +591,29 @@ export default function SponsorsAdvertiseRequestFormAdsSection({
onPrevious();
}}
/>
<Button
icon={RiArrowRightLine}
isDisabled={ads.length === 0}
label={intl.formatMessage({
defaultMessage: 'Company details',
description: 'Label for company details button',
id: 'OY0i/0',
})}
size="md"
variant="primary"
onClick={() => {
onSubmit();
}}
/>
<div className="flex items-start gap-3">
<SponsorsAdvertiseRequestPromoCode
appliedPromoCode={promoCode?.code}
className="mt-0.5"
onApplyPromoCode={(_promoCode) => {
updatePromoCode(_promoCode);
}}
/>
<Button
icon={RiArrowRightLine}
isDisabled={ads.length === 0}
label={intl.formatMessage({
defaultMessage: 'Company details',
description: 'Label for company details button',
id: 'OY0i/0',
})}
size="md"
variant="primary"
onClick={() => {
onSubmit();
}}
/>
</div>
</div>
)}
</Section>

View File

@ -0,0 +1,136 @@
import clsx from 'clsx';
import { useState } from 'react';
import { RiCheckLine } from 'react-icons/ri';
import { useIntl } from '~/components/intl';
import { SponsorsPromoCodeConfig } from '~/components/sponsors/SponsorsPromoCodeConfig';
import Button from '~/components/ui/Button';
import Text from '~/components/ui/Text';
import TextInput from '~/components/ui/TextInput';
import { themeTextSuccessColor } from '~/components/ui/theme';
type Props = Readonly<{
appliedPromoCode?: string;
className?: string;
onApplyPromoCode: ({
code,
percentOff,
}: Readonly<{ code: string; percentOff: number }>) => void;
}>;
export default function SponsorsAdvertiseRequestPromoCode({
appliedPromoCode,
className,
onApplyPromoCode,
}: Props) {
const intl = useIntl();
const label = intl.formatMessage({
defaultMessage: 'Add promotion code',
description: 'Label to add promotion code',
id: 'YQhqx6',
});
const [isValidated, setIsValidated] = useState(!!appliedPromoCode);
const [promoCode, setPromoCode] = useState<{
error: boolean;
percentOff: number | null;
value: string;
}>({
error: false,
percentOff:
SponsorsPromoCodeConfig[appliedPromoCode ?? '']?.percentOff ?? null,
value: appliedPromoCode ?? '',
});
async function handleValidate(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const data = SponsorsPromoCodeConfig[promoCode.value];
setIsValidated(data != null);
if (data) {
const discount = data.percentOff;
if (discount > 0) {
onApplyPromoCode({
code: data.code,
percentOff: discount,
});
}
setPromoCode({
...promoCode,
error: false,
percentOff: data.percentOff,
value: data.code,
});
} else {
setPromoCode({
...promoCode,
error: true,
percentOff: null,
value: promoCode.value,
});
}
}
return (
<div className={clsx('space-y-2', 'w-60', className)}>
<form className="relative" onSubmit={handleValidate}>
<TextInput
className="pr-8"
errorMessage={
promoCode.error
? intl.formatMessage({
defaultMessage: 'Invalid promo code',
description: 'Error message for invalid promotion code',
id: 'isHmCU',
})
: undefined
}
isLabelHidden={true}
label={label}
placeholder={label}
size="sm"
value={promoCode.value}
onChange={(value) => {
setIsValidated(false);
setPromoCode({ ...promoCode, value });
}}
/>
{isValidated ? (
<RiCheckLine
aria-hidden={true}
className={clsx(
'absolute right-3 top-2',
'size-4',
themeTextSuccessColor,
)}
/>
) : (
<Button
className="absolute right-1.5 top-0.5"
icon={RiCheckLine}
isDisabled={promoCode.value.length === 0}
isLabelHidden={true}
label={label}
size="xs"
type="submit"
variant="tertiary"
/>
)}
</form>
{promoCode.percentOff && (
<Text color="secondary" size="body3">
{intl.formatMessage(
{
defaultMessage: 'Code successfully applied! {percentOff}% off',
description: 'Message when promo code is successfully applied',
id: 'Lqq1vK',
},
{ percentOff: promoCode.percentOff },
)}
</Text>
)}
</div>
);
}

View File

@ -250,8 +250,9 @@ export default function SponsorsAdvertiseRequestAgreement({
.
</li>
<li>
The exact amount for this booking is specified in the Booking Form and
is <strong>payable upfront</strong>.
The exact amount for this booking, including any applicable discounts,
is specified in the Booking Form and is{' '}
<strong>payable upfront</strong>.
</li>
</ul>
<p>

View File

@ -5,6 +5,7 @@ import { RiArrowLeftLine, RiArrowRightLine } from 'react-icons/ri';
import type { StepsTabItemStatus } from '~/components/common/StepsTabs';
import { FormattedMessage, useIntl } from '~/components/intl';
import { getDiscountedPrice } from '~/components/purchase/PurchasePricingUtils';
import Anchor from '~/components/ui/Anchor';
import Button from '~/components/ui/Button';
import CheckboxInput from '~/components/ui/CheckboxInput';
@ -22,6 +23,7 @@ import {
import type {
SponsorsAdFormatFormItem,
SponsorsCompanyDetails,
SponsorsPromoCode,
} from '../types';
import SponsorsAdvertiseRequestAgreement from './SponsorsAdvertiseRequestAgreement';
@ -30,6 +32,7 @@ type Props = Readonly<{
ads: Array<SponsorsAdFormatFormItem>;
company: SponsorsCompanyDetails;
emails: Array<string>;
promoCode: SponsorsPromoCode;
};
isSubmitting?: boolean;
mode?: 'create' | 'edit' | 'readonly';
@ -62,7 +65,7 @@ export default function SponsorsAdvertiseRequestFormReviewSection({
mode === 'create' ? false : true,
);
const { ads, company, emails } = data;
const { ads, company, emails, promoCode } = data;
const { address, legalName, signatoryName, signatoryTitle, taxNumber } =
company;
const addressString = [
@ -81,6 +84,13 @@ export default function SponsorsAdvertiseRequestFormReviewSection({
0,
);
const discountedAmount = promoCode?.percentOff
? getDiscountedPrice({
percentOff: promoCode.percentOff,
price: totalAmount,
})
: totalAmount;
useEffect(() => {
if (isReadonly) {
return;
@ -105,7 +115,7 @@ export default function SponsorsAdvertiseRequestFormReviewSection({
authorizedSignatoryName={signatoryName}
authorizedSignatoryTitle={signatoryTitle}
contactEmails={emails}
totalAmount={totalAmount}
totalAmount={discountedAmount}
/>
);
@ -184,10 +194,20 @@ export default function SponsorsAdvertiseRequestFormReviewSection({
</div>
);
})}
{promoCode?.code && (
<div className="flex justify-between">
<Text color="secondary" size="body2">
{promoCode.code}
</Text>
<Text color="secondary" size="body2" weight="medium">
-${totalAmount - discountedAmount}
</Text>
</div>
)}
<Divider className="my-2" />
<div className="flex justify-end">
<Text color="secondary" size="body2" weight="medium">
${totalAmount}
${discountedAmount}
</Text>
</div>
</div>

View File

@ -49,3 +49,8 @@ export type SponsorsCompanyDetails = Readonly<{
signatoryTitle: string;
taxNumber?: string;
}>;
export type SponsorsPromoCode = Readonly<{
code: string;
percentOff: number;
} | null>;

View File

@ -4,12 +4,17 @@ import { v4 as uuidv4 } from 'uuid';
import { useGreatStorageLocal } from '~/hooks/useGreatStorageLocal';
import type { SponsorsAdFormatFormItem, SponsorsCompanyDetails } from './types';
import type {
SponsorsAdFormatFormItem,
SponsorsCompanyDetails,
SponsorsPromoCode,
} from './types';
export type AdvertiseRequestFormValues = Readonly<{
ads: Array<SponsorsAdFormatFormItem>;
company: SponsorsCompanyDetails | null;
emails: Array<string>;
promoCode: SponsorsPromoCode;
sessionId: string;
}>;
@ -28,6 +33,7 @@ export default function useSponsorsAdvertiseRequestFormData(
ads: [],
company: null,
emails: [],
promoCode: null,
sessionId: uuidv4(),
}),
{ ttl: 7 * 24 * 60 * 60 },
@ -39,6 +45,7 @@ export default function useSponsorsAdvertiseRequestFormData(
ads: [],
company: null,
emails: [],
promoCode: null,
sessionId: uuidv4(),
},
);

View File

@ -32,7 +32,7 @@ export const EmailsItemConfigSponsorsAdRequestReview: EmailItemConfig<
signatoryTitle: 'CEO',
},
from: {
email: 'hello@greatfrontend',
email: 'hello@greatfrontend.com',
name: 'GreatFrontEnd Sponsorships',
},
id: 'SPONSORS_AD_REQUEST_REVIEW',

View File

@ -56,12 +56,14 @@ export async function sendSponsorsAdRequestReviewEmail({
adRequestId,
ads,
legalName,
percentOff,
signatoryName,
signatoryTitle,
}: Readonly<{
adRequestId: string;
ads: Array<SponsorsAdFormatFormItem>;
legalName: string;
percentOff?: number | null;
signatoryName: string;
signatoryTitle: string;
}>) {
@ -75,6 +77,7 @@ export async function sendSponsorsAdRequestReviewEmail({
props: {
ads,
legalName,
percentOff,
requestUrl: url.format({
host: getSiteOrigin({ usePreviewForDev: true }),
pathname: `/admin/sponsorships/request/${adRequestId}`,

View File

@ -8,6 +8,7 @@ import {
import { lowerCase, startCase } from 'lodash-es';
import React from 'react';
import { getDiscountedPrice } from '~/components/purchase/PurchasePricingUtils';
import type { SponsorsAdFormatFormItem } from '~/components/sponsors/request/types';
import { SponsorAdFormatConfigs } from '~/components/sponsors/SponsorsAdFormatConfigs';
import {
@ -27,6 +28,7 @@ import { containerStyle, mainStyle } from '~/emails/components/EmailsStyles';
type Props = Readonly<{
ads: Array<SponsorsAdFormatFormItem>;
legalName: string;
percentOff?: number | null;
requestUrl: string;
signatoryName: string;
signatoryTitle: string;
@ -35,6 +37,7 @@ type Props = Readonly<{
export default function EmailsTemplateSponsorsAdRequestReview({
ads,
legalName,
percentOff,
requestUrl,
signatoryName,
signatoryTitle,
@ -42,7 +45,13 @@ export default function EmailsTemplateSponsorsAdRequestReview({
const totalAmount = ads.reduce(
(acc, curr) =>
acc +
curr.weeks.length * SponsorAdFormatConfigs[curr.format].pricePerWeekUSD,
curr.weeks.length *
(percentOff
? getDiscountedPrice({
percentOff,
price: SponsorAdFormatConfigs[curr.format].pricePerWeekUSD,
})
: SponsorAdFormatConfigs[curr.format].pricePerWeekUSD),
0,
);

View File

@ -276,84 +276,94 @@ export const sponsorsRouter = router({
agreement: z.string(),
company: sponsorsCompanySchemaServer,
emails: z.array(z.string().email()),
promoCode: z
.object({
code: z.string(),
percentOff: z.number(),
})
.optional(),
}),
)
.mutation(async ({ input: { ads, agreement, company, emails } }) => {
const { address, legalName, signatoryName, signatoryTitle, taxNumber } =
company;
.mutation(
async ({ input: { ads, agreement, company, emails, promoCode } }) => {
const { address, legalName, signatoryName, signatoryTitle, taxNumber } =
company;
const adRequest = await prisma.sponsorsAdRequest.create({
data: {
address,
ads: {
create: ads.map((ad) => {
const adData = (() => {
switch (ad.format) {
case 'GLOBAL_BANNER':
return {};
case 'IN_CONTENT':
return {
body: ad.body,
imageUrl: ad.imageUrl,
};
case 'SPOTLIGHT':
return {
imageUrl: ad.imageUrl,
};
default:
throw new Error('Invalid ad format');
}
})();
const adRequest = await prisma.sponsorsAdRequest.create({
data: {
address,
ads: {
create: ads.map((ad) => {
const adData = (() => {
switch (ad.format) {
case 'GLOBAL_BANNER':
return {};
case 'IN_CONTENT':
return {
body: ad.body,
imageUrl: ad.imageUrl,
};
case 'SPOTLIGHT':
return {
imageUrl: ad.imageUrl,
};
default:
throw new Error('Invalid ad format');
}
})();
return {
format: ad.format,
sponsorName: ad.sponsorName,
title: ad.text,
url: ad.url,
...adData,
slots: {
create: Array.from(ad.weeks).map((slot) => {
const [year, week] = slot
.split('/')
.map((part) => Number(part));
return {
format: ad.format,
sponsorName: ad.sponsorName,
title: ad.text,
url: ad.url,
...adData,
slots: {
create: Array.from(ad.weeks).map((slot) => {
const [year, week] = slot
.split('/')
.map((part) => Number(part));
return {
week,
year,
};
}),
},
};
}),
return {
week,
year,
};
}),
},
};
}),
},
agreement,
emails,
legalName,
promoCode: promoCode?.code,
signatoryName,
signatoryTitle,
taxNumber,
},
agreement,
emails,
legalName,
signatoryName,
signatoryTitle,
taxNumber,
},
});
});
// Send email to advertiser and sponsor manager
await Promise.all([
sendSponsorsAdRequestConfirmationEmail({
adRequestId: adRequest.id,
email: emails[0],
signatoryName,
}),
sendSponsorsAdRequestReviewEmail({
adRequestId: adRequest.id,
ads: ads.map((ad) => ({
...ad,
id: new Date().toISOString(),
})),
legalName,
signatoryName,
signatoryTitle,
}),
]);
}),
// Send email to advertiser and sponsor manager
await Promise.all([
sendSponsorsAdRequestConfirmationEmail({
adRequestId: adRequest.id,
email: emails[0],
signatoryName,
}),
sendSponsorsAdRequestReviewEmail({
adRequestId: adRequest.id,
ads: ads.map((ad) => ({
...ad,
id: new Date().toISOString(),
})),
legalName,
percentOff: promoCode?.percentOff ?? 0,
signatoryName,
signatoryTitle,
}),
]);
},
),
adRequestInquiries: adminProcedure.query(async () => {
const aplQuery = `
['events']
@ -518,12 +528,26 @@ export const sponsorsRouter = router({
company: sponsorsCompanySchemaServer,
emails: z.array(z.string()),
id: z.string(),
promoCode: z
.object({
code: z.string(),
percentOff: z.number(),
})
.optional(),
}),
)
.mutation(
async ({
ctx: { viewer },
input: { ads, advertiserEmail, agreement, company, emails, id },
input: {
ads,
advertiserEmail,
agreement,
company,
emails,
id,
promoCode,
},
}) => {
// If not admin, check if the user is authorized to update the request
if (!(viewer && ADMIN_EMAILS.includes(viewer.email))) {
@ -553,6 +577,7 @@ export const sponsorsRouter = router({
agreement,
emails,
legalName,
promoCode: promoCode?.code,
signatoryName,
signatoryTitle,
taxNumber,