diff --git a/apps/web/src/components/purchase/PurchasePricingUtils.ts b/apps/web/src/components/purchase/PurchasePricingUtils.ts index 730cb7279..b282bbca7 100644 --- a/apps/web/src/components/purchase/PurchasePricingUtils.ts +++ b/apps/web/src/components/purchase/PurchasePricingUtils.ts @@ -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; +} diff --git a/apps/web/src/components/sponsors/SponsorsPromoCodeConfig.ts b/apps/web/src/components/sponsors/SponsorsPromoCodeConfig.ts new file mode 100644 index 000000000..10b202c3e --- /dev/null +++ b/apps/web/src/components/sponsors/SponsorsPromoCodeConfig.ts @@ -0,0 +1,12 @@ +export const SponsorsPromoCodeConfig: Record< + string, + { + code: string; + percentOff: number; + } +> = { + TRIAL25: { + code: 'TRIAL25', + percentOff: 25, + }, +} as const; diff --git a/apps/web/src/components/sponsors/admin/SponsorsAdminAdRequestPage.tsx b/apps/web/src/components/sponsors/admin/SponsorsAdminAdRequestPage.tsx index bb436a5b2..13f9050eb 100644 --- a/apps/web/src/components/sponsors/admin/SponsorsAdminAdRequestPage.tsx +++ b/apps/web/src/components/sponsors/admin/SponsorsAdminAdRequestPage.tsx @@ -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, }} diff --git a/apps/web/src/components/sponsors/request/SponsorsAdvertiseRequestEditPage.tsx b/apps/web/src/components/sponsors/request/SponsorsAdvertiseRequestEditPage.tsx index 9f3d03fb4..094b647ec 100644 --- a/apps/web/src/components/sponsors/request/SponsorsAdvertiseRequestEditPage.tsx +++ b/apps/web/src/components/sponsors/request/SponsorsAdvertiseRequestEditPage.tsx @@ -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, }} diff --git a/apps/web/src/components/sponsors/request/SponsorsAdvertiseRequestForm.tsx b/apps/web/src/components/sponsors/request/SponsorsAdvertiseRequestForm.tsx index e2f1cc82f..3d20afbca 100644 --- a/apps/web/src/components/sponsors/request/SponsorsAdvertiseRequestForm.tsx +++ b/apps/web/src/components/sponsors/request/SponsorsAdvertiseRequestForm.tsx @@ -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({ 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 || diff --git a/apps/web/src/components/sponsors/request/SponsorsAdvertiseRequestReadonly.tsx b/apps/web/src/components/sponsors/request/SponsorsAdvertiseRequestReadonly.tsx index cb2ee8140..3e5701488 100644 --- a/apps/web/src/components/sponsors/request/SponsorsAdvertiseRequestReadonly.tsx +++ b/apps/web/src/components/sponsors/request/SponsorsAdvertiseRequestReadonly.tsx @@ -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 ( -
+
@@ -188,7 +214,7 @@ export default function SponsorsAdvertiseRequestReadonly({
    {ads.map((ad) => ( - + ))}
@@ -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, }} /> @@ -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 }>) { $ {ad.weeks.length * - SponsorAdFormatConfigs[ad.format].pricePerWeekUSD} + (promoCode + ? getDiscountedPrice({ + percentOff: promoCode.percentOff, + price: SponsorAdFormatConfigs[ad.format].pricePerWeekUSD, + }) + : SponsorAdFormatConfigs[ad.format].pricePerWeekUSD)}
diff --git a/apps/web/src/components/sponsors/request/ads/SponsorsAdvertiseRequestFormAdsSection.tsx b/apps/web/src/components/sponsors/request/ads/SponsorsAdvertiseRequestFormAdsSection.tsx index bec17db5c..b09535516 100644 --- a/apps/web/src/components/sponsors/request/ads/SponsorsAdvertiseRequestFormAdsSection.tsx +++ b/apps/web/src/components/sponsors/request/ads/SponsorsAdvertiseRequestFormAdsSection.tsx @@ -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; mode: 'create' | 'edit' | 'readonly'; onPrevious: () => void; onSubmit: () => void; + promoCode: SponsorsPromoCode; sessionId: string; updateAds(ads: Array): 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({ />
- - $ - {ad.weeks.length * - SponsorAdFormatConfigs[ad.format].pricePerWeekUSD} - +
+ {promoCode?.percentOff && ( + + $ + {ad.weeks.length * + SponsorAdFormatConfigs[ad.format].pricePerWeekUSD} + + )} + + + $ + {ad.weeks.length * + (promoCode?.percentOff + ? getDiscountedPrice({ + percentOff: promoCode?.percentOff, + price: + SponsorAdFormatConfigs[ad.format] + .pricePerWeekUSD, + }) + : SponsorAdFormatConfigs[ad.format] + .pricePerWeekUSD)} + +
{isReadonly ? (
)} diff --git a/apps/web/src/components/sponsors/request/ads/SponsorsAdvertiseRequestPromoCode.tsx b/apps/web/src/components/sponsors/request/ads/SponsorsAdvertiseRequestPromoCode.tsx new file mode 100644 index 000000000..24460defa --- /dev/null +++ b/apps/web/src/components/sponsors/request/ads/SponsorsAdvertiseRequestPromoCode.tsx @@ -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) { + 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 ( +
+
+ { + setIsValidated(false); + setPromoCode({ ...promoCode, value }); + }} + /> + {isValidated ? ( + + ) : ( +
+ ); +} diff --git a/apps/web/src/components/sponsors/request/review/SponsorsAdvertiseRequestAgreement.tsx b/apps/web/src/components/sponsors/request/review/SponsorsAdvertiseRequestAgreement.tsx index 2ca69f6f0..0ccd4e773 100644 --- a/apps/web/src/components/sponsors/request/review/SponsorsAdvertiseRequestAgreement.tsx +++ b/apps/web/src/components/sponsors/request/review/SponsorsAdvertiseRequestAgreement.tsx @@ -250,8 +250,9 @@ export default function SponsorsAdvertiseRequestAgreement({ .
  • - The exact amount for this booking is specified in the Booking Form and - is payable upfront. + The exact amount for this booking, including any applicable discounts, + is specified in the Booking Form and is{' '} + payable upfront.
  • diff --git a/apps/web/src/components/sponsors/request/review/SponsorsAdvertiseRequestFormReviewSection.tsx b/apps/web/src/components/sponsors/request/review/SponsorsAdvertiseRequestFormReviewSection.tsx index db676901f..e1fc9b55b 100644 --- a/apps/web/src/components/sponsors/request/review/SponsorsAdvertiseRequestFormReviewSection.tsx +++ b/apps/web/src/components/sponsors/request/review/SponsorsAdvertiseRequestFormReviewSection.tsx @@ -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; company: SponsorsCompanyDetails; emails: Array; + 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({

    ); })} + {promoCode?.code && ( +
    + + {promoCode.code} + + + -${totalAmount - discountedAmount} + +
    + )}
    - ${totalAmount} + ${discountedAmount}
    diff --git a/apps/web/src/components/sponsors/request/types.ts b/apps/web/src/components/sponsors/request/types.ts index 2f4f02ab7..8381bdd20 100644 --- a/apps/web/src/components/sponsors/request/types.ts +++ b/apps/web/src/components/sponsors/request/types.ts @@ -49,3 +49,8 @@ export type SponsorsCompanyDetails = Readonly<{ signatoryTitle: string; taxNumber?: string; }>; + +export type SponsorsPromoCode = Readonly<{ + code: string; + percentOff: number; +} | null>; diff --git a/apps/web/src/components/sponsors/request/useSponsorsAdvertiseRequestFormData.ts b/apps/web/src/components/sponsors/request/useSponsorsAdvertiseRequestFormData.ts index 5b1ea1b35..8c809e4e6 100644 --- a/apps/web/src/components/sponsors/request/useSponsorsAdvertiseRequestFormData.ts +++ b/apps/web/src/components/sponsors/request/useSponsorsAdvertiseRequestFormData.ts @@ -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; company: SponsorsCompanyDetails | null; emails: Array; + 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(), }, ); diff --git a/apps/web/src/emails/items/sponsors/EmailsItemConfigSponsorsAdRequestReview.ts b/apps/web/src/emails/items/sponsors/EmailsItemConfigSponsorsAdRequestReview.ts index a5985cd78..a2f7c650a 100644 --- a/apps/web/src/emails/items/sponsors/EmailsItemConfigSponsorsAdRequestReview.ts +++ b/apps/web/src/emails/items/sponsors/EmailsItemConfigSponsorsAdRequestReview.ts @@ -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', diff --git a/apps/web/src/emails/items/sponsors/EmailsSenderSponsors.ts b/apps/web/src/emails/items/sponsors/EmailsSenderSponsors.ts index df4bbff0c..b2df8a4cb 100644 --- a/apps/web/src/emails/items/sponsors/EmailsSenderSponsors.ts +++ b/apps/web/src/emails/items/sponsors/EmailsSenderSponsors.ts @@ -56,12 +56,14 @@ export async function sendSponsorsAdRequestReviewEmail({ adRequestId, ads, legalName, + percentOff, signatoryName, signatoryTitle, }: Readonly<{ adRequestId: string; ads: Array; 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}`, diff --git a/apps/web/src/emails/items/sponsors/EmailsTemplateSponsorsAdRequestReview.tsx b/apps/web/src/emails/items/sponsors/EmailsTemplateSponsorsAdRequestReview.tsx index 7cc2b4676..5f0ca1c99 100644 --- a/apps/web/src/emails/items/sponsors/EmailsTemplateSponsorsAdRequestReview.tsx +++ b/apps/web/src/emails/items/sponsors/EmailsTemplateSponsorsAdRequestReview.tsx @@ -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; 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, ); diff --git a/apps/web/src/server/routers/sponsors.tsx b/apps/web/src/server/routers/sponsors.tsx index db37cf266..332fba7a0 100644 --- a/apps/web/src/server/routers/sponsors.tsx +++ b/apps/web/src/server/routers/sponsors.tsx @@ -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,