Skip to content

Commit

Permalink
feat: allow all billing types
Browse files Browse the repository at this point in the history
  • Loading branch information
IanKrieger committed Nov 8, 2023
1 parent b26251a commit ea33176
Show file tree
Hide file tree
Showing 11 changed files with 91 additions and 55 deletions.
3 changes: 2 additions & 1 deletion src/form/FormikHelpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export const FormikRadioGroup = (

interface FormikRadioControlProps {
name: string;
options: Array<{ label: string; value: string | number }>;
options: Array<{ label: string; value: string | number; disabled?: boolean }>;
label?: string;
helperText?: ReactNode;
disabled?: boolean;
Expand All @@ -149,6 +149,7 @@ export const FormikRadioControl = (props: FormikRadioControlProps) => {
key={opt.value}
value={opt.value}
label={opt.label}
disabled={opt.disabled}
control={<Radio />}
/>
))}
Expand Down
3 changes: 0 additions & 3 deletions src/graphql/advertiser.generated.tsx

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

1 change: 0 additions & 1 deletion src/graphql/advertiser.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ fragment AdvertiserPrice on AdvertiserPrice {
billingModelPrice
billingType
format
isPrimaryFormat
}

query advertiserImages($id: String!) {
Expand Down
1 change: 0 additions & 1 deletion src/graphql/types.ts

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

5 changes: 3 additions & 2 deletions src/user/views/adsManager/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,9 @@ export const initialCampaign = (
advertiser: AdvertiserWithPrices,
): CampaignForm => {
const format = CampaignFormat.PushNotification;
const billingType = "cpm";
const price = advertiser.prices.find(
(p) => p.format === format && p.isPrimaryFormat,
(p) => p.format === format && p.billingType === billingType.toUpperCase(),
);
return {
isCreating: false,
Expand All @@ -125,7 +126,7 @@ export const initialCampaign = (
dailyBudget: MIN_PER_CAMPAIGN,
geoTargets: [],
newCreative: initialCreative,
billingType: (price?.billingType.toLowerCase() ?? "cpm") as Billing,
billingType: billingType,
currency: "USD",
price: price?.billingModelPrice ?? "6",
adSets: [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { BudgetField } from "user/views/adsManager/views/advanced/components/campaign/fields/BudgetField";
import { PaymentMethodField } from "user/views/adsManager/views/advanced/components/campaign/fields/PaymentMethodField";
import { AdvertiserPriceFragment } from "graphql/advertiser.generated";

export function BudgetSettings() {
export function BudgetSettings(props: { prices: AdvertiserPriceFragment[] }) {
return (
<>
<BudgetField />
<BudgetField prices={props.prices} />

<PaymentMethodField />
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@ import {
FormikTextField,
useIsEdit,
} from "form/FormikHelpers";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { useField, useFormikContext } from "formik";
import { CampaignForm } from "../../../../../types";
import { Billing, CampaignForm } from "../../../../../types";
import { differenceInHours } from "date-fns";
import { MIN_PER_CAMPAIGN, MIN_PER_DAY } from "validation/CampaignSchema";
import { useAdvertiser } from "auth/hooks/queries/useAdvertiser";
import _ from "lodash";
import { CardContainer } from "components/Card/CardContainer";
import { uiLabelsForBillingType } from "util/billingType";
import { uiTextForCampaignFormat } from "user/library";
import { CampaignFormat } from "graphql/types";
import { AdvertiserPriceFragment } from "graphql/advertiser.generated";

export function BudgetField() {
export function BudgetField(props: { prices: AdvertiserPriceFragment[] }) {
const [, , dailyBudget] = useField<number>("dailyBudget");
const [, , campaignPrice] = useField<string>("price");
const { isDraft } = useIsEdit();
const { advertiser } = useAdvertiser();
const { values, errors } = useFormikContext<CampaignForm>();
Expand All @@ -26,6 +27,18 @@ export function BudgetField() {
differenceInHours(new Date(values.endAt), new Date(values.startAt)) / 24,
);

const isBillingTypeDisabled = useCallback(
(billing: Billing) => {
const billingType = billing.toUpperCase();
return (
props.prices.find(
(p) => p.format === values.format && p.billingType === billingType,
) === undefined
);
},
[values.format, props.prices],
);

useEffect(() => {
const calculatedBudget =
campaignRuntime > 0
Expand Down Expand Up @@ -69,17 +82,38 @@ export function BudgetField() {
disabled={!isDraft && !advertiser.selfServiceSetPrice}
/>

{!advertiser.selfServiceSetPrice ? (
<Typography variant="body2">
{uiTextForCampaignFormat(values.format)} campaigns are priced at a
flat rate of{" "}
<strong>
${values.price} {_.upperCase(values.billingType)}
</strong>
.
</Typography>
) : (
<Stack direction="column" spacing={2} alignItems="flex-start">
<Stack direction="column" spacing={2} alignItems="flex-start">
<FormikRadioControl
name="billingType"
label="Billing Type"
onChange={(e) => {
const billingType = e.target.value.toUpperCase();
const price = props.prices.find(
(p) =>
p.format === values.format && p.billingType === billingType,
);
if (price) campaignPrice.setValue(price?.billingModelPrice);
}}
options={[
{
value: "cpm",
label: uiLabelsForBillingType("cpm").longLabel,
disabled: isBillingTypeDisabled("cpm") && !isDraft,
},
{
value: "cpc",
label: uiLabelsForBillingType("cpc").longLabel,
disabled: isBillingTypeDisabled("cpc") && !isDraft,
},
{
value: "cpqv",
label: uiLabelsForBillingType("cpqv").longLabel,
disabled: isBillingTypeDisabled("cpqv") && !isDraft,
},
]}
/>

{advertiser.selfServiceSetPrice ? (
<FormikTextField
fullWidth={false}
name="price"
Expand All @@ -92,25 +126,14 @@ export function BudgetField() {
}}
disabled={!isDraft}
/>

<FormikRadioControl
name="billingType"
options={[
{
value: "cpm",
label: uiLabelsForBillingType("cpm").longLabel,
},
{
value: "cpc",
label: uiLabelsForBillingType("cpc").longLabel,
},
]}
disabled={
!isDraft || values.format === CampaignFormat.NewsDisplayAd
}
/>
</Stack>
)}
) : (
<Typography variant="body2">
<strong>{_.upperCase(values.billingType)}</strong>{" "}
{uiTextForCampaignFormat(values.format)} campaigns are priced at a
flat rate of <strong>${values.price}</strong>.
</Typography>
)}
</Stack>
</Stack>
</CardContainer>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import _ from "lodash";
import { useIsEdit } from "form/FormikHelpers";
import { AdvertiserPriceFragment } from "graphql/advertiser.generated";
import { FormatHelp } from "components/Button/FormatHelp";
import { Billing } from "user/views/adsManager/types";

interface PriceProps {
prices: AdvertiserPriceFragment[];
Expand Down Expand Up @@ -38,6 +39,7 @@ const FormatItemButton = (props: { format: CampaignFormat } & PriceProps) => {
const { isEdit } = useIsEdit();
const [, meta, format] = useField<CampaignFormat>("format");
const [, , price] = useField<string>("price");
const [, billingType] = useField<Billing>("billingType");

return (
<ListItemButton
Expand All @@ -46,7 +48,9 @@ const FormatItemButton = (props: { format: CampaignFormat } & PriceProps) => {
onClick={() => {
format.setValue(props.format);
const found = props.prices.find(
(p) => p.format === props.format && p.isPrimaryFormat,
(p) =>
p.format === props.format &&
p.billingType === billingType.value.toUpperCase(),
);
if (props.format === CampaignFormat.NewsDisplayAd) {
price.setValue(found?.billingModelPrice ?? "10");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function BaseForm({ hasPaymentIntent, prices }: Props) {
{
label: "Budget",
path: `${url}/budget`,
component: <BudgetSettings />,
component: <BudgetSettings prices={prices} />,
},
{
label: "Ads",
Expand Down
2 changes: 0 additions & 2 deletions src/validation/CampaignSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,11 @@ const prices: Omit<AdvertiserPriceFragment, "isDefault">[] = [
format: CampaignFormat.PushNotification,
billingModelPrice: "6",
billingType: BillingType.Cpm,
isPrimaryFormat: true,
},
{
format: CampaignFormat.PushNotification,
billingModelPrice: ".15",
billingType: BillingType.Cpc,
isPrimaryFormat: true,
},
];

Expand Down
27 changes: 20 additions & 7 deletions src/validation/CampaignSchema.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const CampaignSchema = (prices: AdvertiserPriceFragment[]) =>
then: (schema) =>
findPrice(
prices,
CampaignFormat.PushNotification,
[CampaignFormat.PushNotification],
BillingType.Cpc,
"0.1",
schema,
Expand All @@ -92,7 +92,7 @@ export const CampaignSchema = (prices: AdvertiserPriceFragment[]) =>
then: (schema) =>
findPrice(
prices,
CampaignFormat.PushNotification,
[CampaignFormat.PushNotification],
BillingType.Cpm,
"6",
schema,
Expand All @@ -104,16 +104,30 @@ export const CampaignSchema = (prices: AdvertiserPriceFragment[]) =>
then: (schema) =>
findPrice(
prices,
CampaignFormat.NewsDisplayAd,
[CampaignFormat.NewsDisplayAd],
BillingType.Cpm,
"10",
schema,
),
})
.when(["billingType", "format"], {
is: (b: string, f: CampaignFormat) =>
b === "cpqv" &&
(f === CampaignFormat.NewsDisplayAd ||
CampaignFormat.PushNotification),
then: (schema) =>
findPrice(
prices,
[CampaignFormat.NewsDisplayAd, CampaignFormat.PushNotification],
BillingType.Cpqv,
"1.5",
schema,
),
})
.required("Price is a required field"),
billingType: string()
.label("Pricing Type")
.oneOf(["cpm", "cpc"])
.oneOf(["cpm", "cpc", "cpqv"])
.required("Pricing type is a required field"),
adSets: array()
.min(1)
Expand Down Expand Up @@ -177,14 +191,13 @@ export const CampaignSchema = (prices: AdvertiserPriceFragment[]) =>

export function findPrice(
prices: AdvertiserPriceFragment[],
format: CampaignFormat,
formats: CampaignFormat[],
billingType: BillingType,
defaultPrice: string,
schema: StringSchema<string | undefined, AnyObject, undefined, "">,
) {
const found = prices.find(
(p) =>
p.format === format && p.billingType === billingType && p.isPrimaryFormat,
(p) => formats.includes(p.format) && p.billingType === billingType,
);
const price = BigNumber(found?.billingModelPrice ?? defaultPrice);
return schema.test(
Expand Down

0 comments on commit ea33176

Please sign in to comment.