diff --git a/src/graphql/ad-set.generated.tsx b/src/graphql/ad-set.generated.tsx index 4b947c7e..21e666d7 100644 --- a/src/graphql/ad-set.generated.tsx +++ b/src/graphql/ad-set.generated.tsx @@ -6,6 +6,7 @@ import * as Apollo from "@apollo/client"; const defaultOptions = {} as const; export type AdSetFragment = { id: string; + price?: string | null; createdAt: any; billingType?: string | null; name?: string | null; @@ -131,6 +132,7 @@ export type CreateAdSetMutationVariables = Types.Exact<{ export type CreateAdSetMutation = { createAdSet: { id: string; + price?: string | null; createdAt: any; billingType?: string | null; name?: string | null; @@ -212,6 +214,7 @@ export type UpdateAdSetMutationVariables = Types.Exact<{ export type UpdateAdSetMutation = { updateAdSet: { id: string; + price?: string | null; createdAt: any; billingType?: string | null; name?: string | null; @@ -301,6 +304,7 @@ export const AdFragmentDoc = gql` export const AdSetFragmentDoc = gql` fragment AdSet on AdSet { id + price createdAt billingType name diff --git a/src/graphql/ad-set.graphql b/src/graphql/ad-set.graphql index 08143953..c25d3df0 100644 --- a/src/graphql/ad-set.graphql +++ b/src/graphql/ad-set.graphql @@ -1,5 +1,6 @@ fragment AdSet on AdSet { id + price createdAt billingType name diff --git a/src/graphql/campaign.generated.tsx b/src/graphql/campaign.generated.tsx index ee73e9ab..a0091b7f 100644 --- a/src/graphql/campaign.generated.tsx +++ b/src/graphql/campaign.generated.tsx @@ -36,6 +36,7 @@ export type CampaignFragment = { geoTargets?: Array<{ code: string; name: string }> | null; adSets: Array<{ id: string; + price?: string | null; createdAt: any; billingType?: string | null; name?: string | null; @@ -147,6 +148,7 @@ export type CampaignAdsFragment = { advertiser: { id: string }; adSets: Array<{ id: string; + price?: string | null; createdAt: any; billingType?: string | null; name?: string | null; @@ -258,6 +260,7 @@ export type LoadCampaignQuery = { geoTargets?: Array<{ code: string; name: string }> | null; adSets: Array<{ id: string; + price?: string | null; createdAt: any; billingType?: string | null; name?: string | null; @@ -351,6 +354,7 @@ export type LoadCampaignAdsQuery = { advertiser: { id: string }; adSets: Array<{ id: string; + price?: string | null; createdAt: any; billingType?: string | null; name?: string | null; diff --git a/src/graphql/types.ts b/src/graphql/types.ts index 3e41ef7c..0c03884f 100644 --- a/src/graphql/types.ts +++ b/src/graphql/types.ts @@ -112,10 +112,8 @@ export type CreateAdInput = { creativeId?: InputMaybe; creativeSetId?: InputMaybe; id?: InputMaybe; - /** The price in the owning campaign's currency for each single confirmation of the priceType specified. Note therefore that the caller is responsible for dividing cost-per-mille by 1000. */ - price: Scalars["Numeric"]; - priceType: ConfirmationType; - state?: InputMaybe; + price?: InputMaybe; + priceType?: InputMaybe; webhooks?: InputMaybe>; }; @@ -133,6 +131,8 @@ export type CreateAdSetInput = { negativeKeywords?: InputMaybe>; oses?: InputMaybe>; perDay: Scalars["Float"]; + /** The price in the owning campaign's currency for each single confirmation of the priceType specified. Note therefore that the caller is responsible for dividing cost-per-mille by 1000. */ + price?: InputMaybe; segments: Array; splitTestGroup?: InputMaybe; state?: InputMaybe; @@ -413,6 +413,8 @@ export type UpdateAdSetInput = { optimized?: InputMaybe; oses?: InputMaybe>; perDay?: InputMaybe; + /** The price in the owning campaign's currency for each single confirmation of the priceType specified. Note therefore that the caller is responsible for dividing cost-per-mille by 1000. */ + price?: InputMaybe; segments?: InputMaybe>; splitTestGroup?: InputMaybe; state?: InputMaybe; diff --git a/src/user/library/index.test.ts b/src/user/library/index.test.ts index 4c468ba4..762b3dd9 100644 --- a/src/user/library/index.test.ts +++ b/src/user/library/index.test.ts @@ -2,9 +2,9 @@ import { CampaignFragment } from "graphql/campaign.generated"; import { describe, expect, it } from "vitest"; import { editCampaignValues, - transformCreative, transformEditForm, transformNewForm, + transformPrice, } from "."; import { CampaignFormat, @@ -18,6 +18,7 @@ import { AdSetForm, CampaignForm, Creative } from "user/views/adsManager/types"; import _ from "lodash"; import { AdFragment, AdSetFragment } from "graphql/ad-set.generated"; import { CreativeFragment } from "graphql/creative.generated"; +import { DeepPartial } from "@apollo/client/utilities"; const BASE_CPM_CAMPAIGN_FRAGMENT: Readonly = { id: "3495317a-bb47-4daf-8d3e-14cdc0e87457", @@ -50,6 +51,7 @@ const BASE_CPM_CAMPAIGN_FRAGMENT: Readonly = { { id: "39644642-b56a-430a-90f8-8917651bb62f", createdAt: "2023-07-11T16:13:31.286Z", + price: "0.006", billingType: "cpm", name: "Demo ad set", totalMax: 10, @@ -130,14 +132,11 @@ describe("pricing logic (read)", () => { const campaign = produce(BASE_CPM_CAMPAIGN_FRAGMENT, (c) => { c.adSets.forEach((adset) => { adset.billingType = "cpm"; - adset.ads?.forEach((ad) => { - ad.price = "0.007"; - ad.priceType = ConfirmationType.View; - }); + adset.price = "0.007"; }); }); const campaignForm = editCampaignValues(campaign, "abc"); - expect(campaignForm.price).toEqual(7); + expect(campaignForm.price).toEqual("7"); expect(campaignForm.billingType).toEqual("cpm"); }); @@ -145,14 +144,11 @@ describe("pricing logic (read)", () => { const campaign = produce(BASE_CPM_CAMPAIGN_FRAGMENT, (c) => { c.adSets.forEach((adset) => { adset.billingType = "cpc"; - adset.ads?.forEach((ad) => { - ad.price = "1"; - ad.priceType = ConfirmationType.View; - }); + adset.price = "1"; }); }); const campaignForm = editCampaignValues(campaign, "abc"); - expect(campaignForm.price).toEqual(1); + expect(campaignForm.price).toEqual("1"); expect(campaignForm.billingType).toEqual("cpc"); }); @@ -161,53 +157,37 @@ describe("pricing logic (read)", () => { c.adSets = []; }); const formObject = editCampaignValues(campaign, "abc"); - expect(formObject.price).toEqual(100); + expect(formObject.price).toEqual("100"); expect(formObject.billingType).toEqual("cpm"); }); }); describe("pricing logic (write)", () => { - const creative: Creative = { - payloadNotification: { - title: "some title", - body: "body", - targetUrl: "some url", - }, - advertiserId: "some id", - state: "draft", - type: { code: "notification_all_v1" }, - name: "some name", - included: true, - }; - it("should convert from CPM to per-impression values when populating a CPM creative", () => { - const inputObject = transformCreative(creative, { + const result = transformPrice({ billingType: "cpm", - price: 9, + price: "9", }); - expect(inputObject.price).toEqual("0.009"); - expect(inputObject.priceType).toEqual(ConfirmationType.View); + expect(result).toEqual("0.009"); }); it("should not convert CPC to per-impression values when populating a CPC creative", () => { - const inputObject = transformCreative(creative, { + const result = transformPrice({ billingType: "cpc", - price: 9, + price: "9", }); - expect(inputObject.price).toEqual("9"); - expect(inputObject.priceType).toEqual(ConfirmationType.Click); + expect(result).toEqual("9"); }); it("should not convert CPV to per-impression values when populating a CPV creative", () => { - const inputObject = transformCreative(creative, { + const result = transformPrice({ billingType: "cpv", - price: 9, + price: "9", }); - expect(inputObject.price).toEqual("9"); - expect(inputObject.priceType).toEqual(ConfirmationType.Landed); + expect(result).toEqual("9"); }); }); @@ -254,7 +234,7 @@ describe("new form tests", () => { isCreating: false, name: "Test", paymentType: PaymentType.Radom, - price: 6, + price: "6", startAt: dateString, state: "draft", type: "paid", @@ -270,8 +250,6 @@ describe("new form tests", () => { "ads": [ { "creativeId": "11111", - "price": "0.006", - "priceType": "VIEW", }, ], "billingType": "cpm", @@ -284,6 +262,7 @@ describe("new form tests", () => { }, ], "perDay": 4, + "price": "0.006", "segments": [ { "code": "5678", @@ -316,29 +295,6 @@ describe("new form tests", () => { } `); }); - - it("should transform a creative", () => { - creative.payloadNotification = { - title: "valid", - targetUrl: "valid", - body: "valid", - }; - - creative.payloadSearch = { - title: "invalid", - targetUrl: "invalid", - body: "invalid", - }; - - const res = transformCreative(creative, form); - expect(res).toMatchInlineSnapshot(` - { - "creativeId": "11111", - "price": "0.006", - "priceType": "VIEW", - } - `); - }); }); describe("edit form tests", () => { @@ -356,36 +312,23 @@ describe("edit form tests", () => { type: { code: "notification_v1_all" }, }; - const ad: AdFragment = { + const ad: Partial = { id: "1", creative: creative, - state: "active", - price: "6", - priceType: ConfirmationType.View, }; - const ad2: AdFragment = { - id: "2", - creative: creative, - state: "deleted", - price: "6", - priceType: ConfirmationType.View, - }; - - const ad3: AdFragment = { + const ad2: Partial = { id: "3", creative: { ...creative, id: "1235", name: "a different creative", }, - state: "active", - price: "6", - priceType: ConfirmationType.View, }; - const adSet: AdSetFragment = { + const adSet: DeepPartial = { ads: [ad, ad2], + price: "6", billingType: "cpm", conversions: [], createdAt: undefined, @@ -397,8 +340,9 @@ describe("edit form tests", () => { totalMax: 100, }; - const adSet2: AdSetFragment = { - ads: [ad, ad3], + const adSet2: DeepPartial = { + ads: [ad], + price: "6", billingType: "cpm", conversions: [], createdAt: undefined, @@ -410,7 +354,7 @@ describe("edit form tests", () => { totalMax: 100, }; - const campaignFragment: CampaignFragment = { + const campaignFragment: DeepPartial = { adSets: [adSet, adSet2], advertiser: { id: "12345" }, budget: 100, @@ -436,8 +380,8 @@ describe("edit form tests", () => { }; const editForm = editCampaignValues( - campaignFragment, - campaignFragment.advertiser.id, + campaignFragment as CampaignFragment, + campaignFragment?.advertiser?.id ?? "", ); it("should result in a valid campaign form", () => { const omitted = _.omit(editForm, ["newCreative"]); @@ -469,7 +413,7 @@ describe("edit form tests", () => { "advertiserId": "12345", "createdAt": undefined, "id": "1235", - "included": false, + "included": true, "name": "a different creative", "payloadInlineContent": undefined, "payloadNotification": { @@ -525,7 +469,7 @@ describe("edit form tests", () => { "advertiserId": "12345", "createdAt": undefined, "id": "1235", - "included": true, + "included": false, "name": "a different creative", "payloadInlineContent": undefined, "payloadNotification": { @@ -569,7 +513,7 @@ describe("edit form tests", () => { "isCreating": false, "name": "My first campaign", "paymentType": "RADOM", - "price": 6000, + "price": "6000", "startAt": undefined, "state": "active", "type": "paid", @@ -588,10 +532,13 @@ describe("edit form tests", () => { { "creativeId": "1234", "creativeSetId": "11111", - "price": "6", - "priceType": "VIEW", + }, + { + "creativeId": "1235", + "creativeSetId": "11111", }, ], + "billingType": "cpm", "id": "11111", "oses": [ { @@ -599,6 +546,7 @@ describe("edit form tests", () => { "name": "macos", }, ], + "price": "6", "segments": [ { "code": "5678", @@ -611,16 +559,9 @@ describe("edit form tests", () => { { "creativeId": "1234", "creativeSetId": "22222", - "price": "6", - "priceType": "VIEW", - }, - { - "creativeId": "1235", - "creativeSetId": "22222", - "price": "6", - "priceType": "VIEW", }, ], + "billingType": "cpm", "id": "22222", "oses": [ { @@ -628,6 +569,7 @@ describe("edit form tests", () => { "name": "linux", }, ], + "price": "6", "segments": [ { "code": "5678", diff --git a/src/user/library/index.ts b/src/user/library/index.ts index 4543e1de..63d2f8ba 100644 --- a/src/user/library/index.ts +++ b/src/user/library/index.ts @@ -1,8 +1,6 @@ import { CampaignFormat, CampaignPacingStrategies, - ConfirmationType, - CreateAdInput, CreateCampaignInput, UpdateCampaignInput, } from "graphql/types"; @@ -52,6 +50,7 @@ export function transformNewForm( budget: form.budget, adSets: form.adSets.map((adSet) => ({ name: adSet.name, + price: transformPrice(form), billingType: form.billingType, perDay: form.format === CampaignFormat.PushNotification ? 4 : 6, segments: adSet.segments.map((s) => ({ code: s.code, name: s.name })), @@ -60,12 +59,21 @@ export function transformNewForm( conversions: transformConversion(adSet.conversions), ads: adSet.creatives .filter((c) => c.included) - .map((ad) => transformCreative(ad, form)), + .map((ad) => ({ creativeId: ad.id })), })), paymentType: form.paymentType, }; } +export const transformPrice = ( + f: Pick, +) => { + const price = BigNumber(f.price); + return f.billingType === "cpm" + ? price.dividedBy(1000).toString() + : price.toString(); +}; + function transformConversion(conv: Conversion[]) { if (conv.length <= 0) { return []; @@ -78,46 +86,15 @@ function transformConversion(conv: Conversion[]) { })); } -export function transformCreative( - creative: Creative, - campaign: Pick, -): CreateAdInput { - let price: BigNumber; - let priceType: ConfirmationType; - - if (campaign.billingType === "cpm") { - price = BigNumber(campaign.price).dividedBy(1000); - priceType = ConfirmationType.View; - } else if (campaign.billingType === "cpv") { - price = BigNumber(campaign.price); - priceType = ConfirmationType.Landed; - } else { - price = BigNumber(campaign.price); - priceType = ConfirmationType.Click; - } - - const createInput: CreateAdInput = { - price: price.toString(), - priceType: priceType, - }; - - createInput.creativeId = creative.id; - - return createInput; -} - export function editCampaignValues( campaign: CampaignFragment, advertiserId: string, ): CampaignForm { - const ads: AdFragment[] = _.filter( - _.flatMap(campaign.adSets, "ads"), - (a) => a.state !== "deleted", - ); + const ads: AdFragment[] = _.flatMap(campaign.adSets, "ads"); const billingType = (_.head(campaign.adSets)?.billingType ?? "cpm") as Billing; - const rawPrice = BigNumber(_.head(ads)?.price ?? "0.1"); + const rawPrice = BigNumber(_.head(campaign.adSets)?.price ?? "0.1"); const price = billingType === "cpm" ? rawPrice.multipliedBy(1000) : rawPrice; return { @@ -147,7 +124,7 @@ export function editCampaignValues( advertiserId, newCreative: initialCreative, currency: campaign.currency, - price: price.toNumber(), + price: price.toString(), billingType: billingType, validateStart: false, budget: campaign.budget, @@ -238,12 +215,14 @@ export function transformEditForm( paymentType: form.paymentType, adSets: form.adSets.map((adSet) => ({ id: adSet.id, + billingType: form.billingType, + price: transformPrice(form), segments: adSet.segments.map((v) => ({ code: v.code, name: v.name })), oses: adSet.oses.map((v) => ({ code: v.code, name: v.name })), ads: adSet.creatives .filter((c) => c.included) .map((ad) => ({ - ...transformCreative(ad, form), + creativeId: ad.id, creativeSetId: adSet.id, })), })), diff --git a/src/user/views/adsManager/types/index.ts b/src/user/views/adsManager/types/index.ts index f2817326..a04d8626 100644 --- a/src/user/views/adsManager/types/index.ts +++ b/src/user/views/adsManager/types/index.ts @@ -24,7 +24,7 @@ export type CampaignForm = { state: string; type: "paid"; // this is per click for CPC campaigns, but per thousand views for CPM campaigns - price: number; + price: string; billingType: Billing; paymentType: PaymentType; }; @@ -118,7 +118,7 @@ export const initialCampaign = (advertiser: IAdvertiser): CampaignForm => { newCreative: initialCreative, billingType: "cpm", currency: "USD", - price: 6, + price: "6", adSets: [ { ...initialAdSet,