diff --git a/internal/cli/resources.go b/internal/cli/resources.go index f6bc5b4..89b8fd3 100644 --- a/internal/cli/resources.go +++ b/internal/cli/resources.go @@ -90,16 +90,15 @@ func campaignCreate(ctx *appContext) *cobra.Command { return err } body := map[string]any{ - "name": name, - "adamId": app.ID, - "dailyBudgetAmount": money(dailyBudget, app.DefaultCurrency), - "countriesOrRegions": parseCSV(countries), - "status": "ENABLED", - "adChannelType": "SEARCH", - "billingEvent": "TAPS", - "supplySources": []string{"APPSTORE_SEARCH_RESULTS"}, - "budgetAmount": nil, - "campaignDisplayHint": "daily budgets are preferred because Apple retires lifetime budgets on 2026-06-16", + "name": name, + "adamId": app.ID, + "dailyBudgetAmount": money(dailyBudget, app.DefaultCurrency), + "countriesOrRegions": parseCSV(countries), + "status": "ENABLED", + "adChannelType": "SEARCH", + "billingEvent": "TAPS", + "supplySources": []string{"APPSTORE_SEARCH_RESULTS"}, + "budgetAmount": nil, } if !apply { return ctx.Print(dryRunPayload("POST", "/campaigns", body)) @@ -398,7 +397,13 @@ func adGroupCreate(ctx *appContext) *cobra.Command { Short: "Create an ad group", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - body := map[string]any{"name": name, "status": "ENABLED", "defaultBidAmount": money(bid, ctx.DefaultCurrency()), "automatedKeywordsOptIn": searchMatch} + body := map[string]any{ + "name": name, + "status": "ENABLED", + "pricingModel": "CPC", + "defaultBidAmount": money(bid, ctx.DefaultCurrency()), + "automatedKeywordsOptIn": searchMatch, + } path := fmt.Sprintf("/campaigns/%s/adgroups", args[0]) if !apply { return ctx.Print(dryRunPayload("POST", path, body)) @@ -621,8 +626,12 @@ func keywordBidCommand(ctx *appContext, use, short string) *cobra.Command { Short: short, Args: cobra.ExactArgs(3), RunE: func(cmd *cobra.Command, args []string) error { - body := map[string]any{"bidAmount": money(bid, ctx.DefaultCurrency())} - path := fmt.Sprintf("/campaigns/%s/adgroups/%s/targetingkeywords/%s", args[0], args[1], args[2]) + keywordID, err := strconv.ParseInt(args[2], 10, 64) + if err != nil { + return fmt.Errorf("keyword-id must be numeric: %w", err) + } + body := []map[string]any{{"id": keywordID, "bidAmount": money(bid, ctx.DefaultCurrency())}} + path := fmt.Sprintf("/campaigns/%s/adgroups/%s/targetingkeywords/bulk", args[0], args[1]) if !apply { return ctx.Print(dryRunPayload("PUT", path, body)) } diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go index 9e1f0d2..bc51ada 100644 --- a/internal/cli/root_test.go +++ b/internal/cli/root_test.go @@ -88,6 +88,9 @@ func TestCampaignCreateDryRunUsesConfiguredApp(t *testing.T) { if body["adamId"].(float64) != 123456 { t.Fatalf("expected adamId from app config, got %#v", body) } + if _, ok := body["campaignDisplayHint"]; ok { + t.Fatalf("Apple campaign create payload must not include local-only hints: %#v", body) + } if _, err := os.Stat(filepath.Join(configDir, "apps.json")); err != nil { t.Fatal("expected apps config to be written") } @@ -110,13 +113,50 @@ func TestKeywordBidDryRunUsesConfiguredCurrency(t *testing.T) { if err := json.Unmarshal(bytes.TrimSpace(stdout.Bytes()), &payload); err != nil { t.Fatalf("stdout was not JSON: %v\n%s", err, stdout.String()) } - body := payload["body"].(map[string]any) - bid := body["bidAmount"].(map[string]any) + if payload["path"] != "/campaigns/1/adgroups/2/targetingkeywords/bulk" { + t.Fatalf("expected bulk keyword update path, got %#v", payload) + } + body := payload["body"].([]any) + if len(body) != 1 { + t.Fatalf("expected one bulk update item, got %#v", body) + } + item := body[0].(map[string]any) + if item["id"].(float64) != 3 { + t.Fatalf("expected numeric keyword id in bulk payload, got %#v", item) + } + bid := item["bidAmount"].(map[string]any) if bid["currency"] != "AUD" { t.Fatalf("expected configured AUD currency, got %#v", body) } } +func TestAdGroupCreateDryRunUsesAppleRequiredFields(t *testing.T) { + configDir := t.TempDir() + var stdout, stderr bytes.Buffer + code := Execute([]string{"config", "app", "add", "--app-id", "123456", "--name", "My App", "--currency", "AUD", "--config-dir", configDir, "--json"}, &stdout, &stderr) + if code != 0 { + t.Fatalf("config app add failed: %s", stderr.String()) + } + stdout.Reset() + stderr.Reset() + code = Execute([]string{"adgroups", "create", "1", "--name", "Category-Broad", "--bid", "2", "--config-dir", configDir, "--json"}, &stdout, &stderr) + if code != 0 { + t.Fatalf("adgroups create dry-run failed: %s", stderr.String()) + } + var payload map[string]any + if err := json.Unmarshal(bytes.TrimSpace(stdout.Bytes()), &payload); err != nil { + t.Fatalf("stdout was not JSON: %v\n%s", err, stdout.String()) + } + body := payload["body"].(map[string]any) + if body["pricingModel"] != "CPC" { + t.Fatalf("expected required CPC pricing model, got %#v", body) + } + bid := body["defaultBidAmount"].(map[string]any) + if bid["amount"] != "2.00" || bid["currency"] != "AUD" { + t.Fatalf("expected AUD default bid payload, got %#v", body) + } +} + func TestKeywordAddDryRunUsesAppleBulkArrayPayload(t *testing.T) { configDir := t.TempDir() var stdout, stderr bytes.Buffer