Skip to content

Commit

Permalink
feat: allow price per advertiser (#900)
Browse files Browse the repository at this point in the history
* feat: allow for price-per-advertiser

* fix: tests

* fix: add default

* fix: update for new pricing structure

* fix: no need for default, use prices when switching

* test: validate price schema

* test: validate price schema pt 2

* fix: flaky test
  • Loading branch information
IanKrieger authored Sep 28, 2023
1 parent b333376 commit 1639804
Show file tree
Hide file tree
Showing 15 changed files with 497 additions and 188 deletions.
3 changes: 1 addition & 2 deletions src/form/fragmentUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,12 @@ export function createAdSetFromFragment(
campaignId?: string,
): CreateAdSetInput {
const ads = (data.ads ?? []).filter((ad) => ad.state !== "deleted");

return {
campaignId,
ads: ads.map((ad) => ({
creativeId: ad.creative.id,
})),
price: ads[0].price,
price: ads[0].price ?? "6",
bannedKeywords: data.bannedKeywords,
billingType: data.billingType ?? "cpm",
conversions: (data.conversions ?? []).map((c) => ({
Expand Down
93 changes: 93 additions & 0 deletions src/graphql/advertiser.generated.tsx

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

14 changes: 14 additions & 0 deletions src/graphql/advertiser.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ fragment AdvertiserImage on AdvertiserImage {
createdAt
}

fragment AdvertiserPrice on AdvertiserPrice {
price
billingType
format
}

query advertiserImages($id: String!) {
advertiser(id: $id) {
images {
Expand All @@ -73,6 +79,14 @@ query advertiserImages($id: String!) {
}
}

query advertiserPrices($id: String!) {
advertiser(id: $id) {
prices {
...AdvertiserPrice
}
}
}

mutation uploadAdvertiserImage($input: CreateAdvertiserImageInput!) {
createAdvertiserImage(createImageInput: $input) {
name
Expand Down
18 changes: 15 additions & 3 deletions src/graphql/types.ts

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

41 changes: 41 additions & 0 deletions src/user/hooks/useAdvertiserWithPrices.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useAdvertiser } from "auth/hooks/queries/useAdvertiser";
import {
AdvertiserPriceFragment,
useAdvertiserPricesQuery,
} from "graphql/advertiser.generated";
import { useState } from "react";
import { IAdvertiser } from "auth/context/auth.interface";
import _ from "lodash";

export type AdvertiserWithPrices = IAdvertiser & {
prices: AdvertiserPriceFragment[];
};
export function useAdvertiserWithPrices() {
const { advertiser } = useAdvertiser();
const [data, setData] = useState<AdvertiserWithPrices>({
...advertiser,
prices: [],
});
const [error, setError] = useState<string>();

const { loading } = useAdvertiserPricesQuery({
variables: { id: advertiser.id },
onCompleted(data) {
const prices = data.advertiser?.prices ?? [];
if (_.isEmpty(prices)) {
setError("Unable to create a new campaign");
return;
}

setData({
...advertiser,
prices,
});
},
onError(error) {
setError(error.message);
},
});

return { data, loading, error };
}
16 changes: 14 additions & 2 deletions src/user/library/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,13 @@ describe("edit form tests", () => {
);
it("should result in a valid campaign form", () => {
const omitted = _.omit(editForm, ["newCreative"]);
expect(omitted).toMatchInlineSnapshot(`
const sorted = {
...omitted,
adSets: omitted.adSets?.sort(
(a, b) => a.id?.localeCompare(b.id ?? "") ?? 1,
),
};
expect(sorted).toMatchInlineSnapshot(`
{
"adSets": [
{
Expand Down Expand Up @@ -528,7 +534,13 @@ describe("edit form tests", () => {

it("should resolve to update input", () => {
const update = transformEditForm(editForm, editForm.id ?? "");
expect(update).toMatchInlineSnapshot(`
const sorted = {
...update,
adSets: update.adSets?.sort(
(a, b) => a.id?.localeCompare(b.id ?? "") ?? 1,
),
};
expect(sorted).toMatchInlineSnapshot(`
{
"adSets": [
{
Expand Down
17 changes: 12 additions & 5 deletions src/user/views/adsManager/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CampaignFormat, CreativeInput, PaymentType } from "graphql/types";
import { defaultEndDate, defaultStartDate } from "form/DateFieldHelpers";
import { MIN_PER_CAMPAIGN } from "validation/CampaignSchema";
import { IAdvertiser } from "auth/context/auth.interface";
import { AdvertiserWithPrices } from "user/hooks/useAdvertiserWithPrices";

export type Billing = "cpm" | "cpc" | "cpv";

Expand Down Expand Up @@ -106,7 +106,14 @@ export const initialAdSet: AdSetForm = {
creatives: [],
};

export const initialCampaign = (advertiser: IAdvertiser): CampaignForm => {
export const initialCampaign = (
advertiser: AdvertiserWithPrices,
): CampaignForm => {
const format = CampaignFormat.PushNotification;
const billingType = "cpm";
const price = advertiser.prices.find(
(p) => p.billingType === billingType.toUpperCase() && p.format === format,
);
return {
isCreating: false,
advertiserId: advertiser.id,
Expand All @@ -117,15 +124,15 @@ export const initialCampaign = (advertiser: IAdvertiser): CampaignForm => {
dailyBudget: MIN_PER_CAMPAIGN,
geoTargets: [],
newCreative: initialCreative,
billingType: "cpm",
billingType,
currency: "USD",
price: "6",
price: price?.price ?? "6",
adSets: [
{
...initialAdSet,
},
],
format: CampaignFormat.PushNotification,
format,
name: "",
state: "draft",
type: "paid",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { CampaignDateRange } from "components/Campaigns/CampaignDateRange";
import { LocationField } from "user/views/adsManager/views/advanced/components/campaign/fields/LocationField";
import { Typography } from "@mui/material";
import { FormatField } from "user/views/adsManager/views/advanced/components/campaign/fields/FormatField";
import { AdvertiserPriceFragment } from "graphql/advertiser.generated";

export function CampaignSettings() {
export function CampaignSettings(props: { prices: AdvertiserPriceFragment[] }) {
const { isDraft } = useIsEdit();

return (
Expand All @@ -20,7 +21,7 @@ export function CampaignSettings() {
<CampaignDateRange />
</CardContainer>

<FormatField />
<FormatField prices={props.prices} />

{isDraft && <LocationField />}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,8 @@ import { uiLabelsForBillingType } from "util/billingType";
import { uiTextForCampaignFormat } from "user/library";
import { CampaignFormat } from "graphql/types";

type DefaultPrice = { cpm: number; cpc: number };
const campaignDefaultPrices = new Map<CampaignFormat, DefaultPrice>([
[CampaignFormat.PushNotification, { cpm: 6, cpc: 0.1 }],
[CampaignFormat.NewsDisplayAd, { cpm: 10, cpc: 0.15 }],
]);

export function BudgetField() {
const [, , dailyBudget] = useField<number>("dailyBudget");
const [, , price] = useField<number>("price");
const { isDraft } = useIsEdit();
const { advertiser } = useAdvertiser();
const { values, errors } = useFormikContext<CampaignForm>();
Expand Down Expand Up @@ -102,13 +95,6 @@ export function BudgetField() {

<FormikRadioControl
name="billingType"
onChange={(e) => {
const defaultPrice = campaignDefaultPrices.get(values.format);
if (defaultPrice)
price.setValue(
defaultPrice[e.target.value as keyof DefaultPrice],
);
}}
options={[
{
value: "cpm",
Expand Down
Loading

0 comments on commit 1639804

Please sign in to comment.