Skip to content

feat(campaigns): typed flags for CPA strategies (Goals, Notification, TimeTargeting) (#204)#219

Merged
axisrow merged 3 commits into
mainfrom
worktree-issue-204-campaigns-cpa-flags
May 21, 2026
Merged

feat(campaigns): typed flags for CPA strategies (Goals, Notification, TimeTargeting) (#204)#219
axisrow merged 3 commits into
mainfrom
worktree-issue-204-campaigns-cpa-flags

Conversation

@axisrow

@axisrow axisrow commented May 21, 2026

Copy link
Copy Markdown
Owner

Summary

Adds typed flags to direct campaigns add for issue #204 — minimum-viable surface for the CPA / PayForConversion strategy use-case that downstream MCP (yandex-direct-mcp-plugin B3) currently can't complete without manual cleanup in the Direct web UI.

New flags (1:1 with WSDL)

Flag WSDL path Notes
--goal-id INT Strategy*Add.GoalId (single) For AVERAGE_CPA, PAY_FOR_CONVERSION_CRR, AVERAGE_CPA_PER_CAMPAIGN, AVERAGE_CPA_PER_FILTER
--priority-goals goal_id:value,… TextCampaign.PriorityGoals.Items[] (PriorityGoalsItem) For *_MULTIPLE_GOALS subtypes — note WSDL fields are GoalId/Value, not Id/Weight
--average-cpa MICRO_RUBLES Strategy*Add.AverageCpa AVERAGE_CPA / AVERAGE_CPA_PER_CAMPAIGN only
--bid-ceiling MICRO_RUBLES Strategy*Add.BidCeiling Only on subtypes that declare it (rejected for PayForConversionCrr and PayForConversionMultipleGoals per WSDL)
--counter-ids INT,… TextCampaign/DynamicTextCampaign.CounterIds SMART_CAMPAIGN uses singular --counter-id instead
--notification JSON CampaignBase.Notification {SmsSettings:{Events,TimeFrom,TimeTo}, EmailSettings:{Email,CheckPositionInterval,WarningBalance,SendAccountNews,SendWarnings}} — unknown keys rejected
--time-targeting JSON CampaignAddItem.TimeTargeting (TimeTargetingAdd) {Schedule, ConsiderWorkingWeekends, HolidaysSchedule:{SuspendOnHolidays,BidPercent,StartHour,EndHour}} — unknown keys rejected

Strategy compatibility is enforced via UsageError at CLI level — invalid combinations never reach the API.

Issue-requested but NOT shipped (WSDL audit)

  • --goals (array) — WSDL Strategy*Add declares only scalar GoalId; multi-goal CPA is via PriorityGoals.Items[] instead, so the array surface is --priority-goals.
  • --network-settings — no NetworkSettings field exists on CampaignAddItem / TextCampaignAddItem / DynamicTextCampaignAddItem / SmartCampaignAddItem in tests/wsdl_cache/campaigns.xml. Probably a misnomer in the issue (possibly confused with Settings or PlacementTypes).

The CHANGELOG **Notes:** section documents the deviation from the original issue wording.

Tests (34 new)

  • 8 positive payload-shape tests (search-strategy CPA, network-strategy CPA, PriorityGoals, CounterIds, Notification, TimeTargeting, Dynamic+CPA, Smart regression).
  • 24 negative-path tests (incompatible flags, bad JSON, unknown keys, missing values).
  • 3 new PAYLOAD_CASES for tests/api_coverage_payloads.py (CPA single-goal, multi-goal, Notification+TimeTargeting).

Two verifier-found WSDL bugs caught and fixed before PR:

  • BidCeiling removed from _STRATEGY_SUPPORTS_BID_CEILING["PayForConversionCrr"] (no such field in StrategyPayForConversionCrrAdd).
  • BidCeiling injection for PayForConversionMultipleGoals gated by _STRATEGY_SUPPORTS_BID_CEILING (no such field in StrategyPayForConversionMultipleGoalsAdd).

Test plan

  • pytest tests/test_dry_run.py -k campaigns_add — 34 PASS
  • pytest tests/test_api_coverage.py tests/test_wsdl_parity_gate.py — 209 PASS
  • flake8 direct_cli/commands/campaigns.py direct_cli/utils.py tests/test_dry_run.py — clean
  • python -m direct_cli.cli campaigns add --help — new flags visible, --goals / --network-settings absent
  • CI: lint + tests on 3.9 / 3.11 / 3.13

Closes #204.

🤖 Generated with Claude Code

… TimeTargeting)

Adds typed flags to `direct campaigns add` for issue #204:

- `--goal-id` / `--priority-goals goal_id:value,…` — single goal vs.
  WSDL `TextCampaignAddItem.PriorityGoals.Items[]` (PriorityGoalsItem
  with GoalId/Value, not Id/Weight).
- `--average-cpa MICRO_RUBLES`, `--bid-ceiling MICRO_RUBLES`,
  `--counter-ids` for Text/Dynamic.
- `--notification JSON` (CampaignBase.Notification with
  SmsSettings/EmailSettings shape validation).
- `--time-targeting JSON` (CampaignAddItem.TimeTargeting with
  HolidaysSchedule shape validation).

Strategy-subtype compatibility enforced via UsageError at CLI:
* --average-cpa rejected outside AVERAGE_CPA / AVERAGE_CPA_PER_CAMPAIGN
* --bid-ceiling rejected for subtypes without BidCeiling
  (PayForConversionCrr, PayForConversionMultipleGoals)
* --priority-goals rejected outside *_MULTIPLE_GOALS subtypes
* --counter-ids rejected for SMART_CAMPAIGN (uses singular --counter-id)

Issue-requested `--goals` (array) and `--network-settings` intentionally
NOT shipped: WSDL has no Goals array (only scalar GoalId, multi-goal via
PriorityGoals) and no NetworkSettings under CampaignAddItem. CHANGELOG
documents this.

Closes #204.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Copilot AI review requested due to automatic review settings May 21, 2026 06:30
@axisrow

axisrow commented May 21, 2026

Copy link
Copy Markdown
Owner Author

@claude review. Focus on critical issues: bugs, security vulnerabilities, logical errors, data loss risks, performance problems. Do NOT nitpick style, naming conventions, minor formatting, or subjective preferences — only flag issues that could break functionality or cause real harm in production.

@claude

claude Bot commented May 21, 2026

Copy link
Copy Markdown

Claude finished @axisrow's task in 4m 38s —— View job


Claude PR Review — Critical Issues Only

  • Read changed files
  • Cross-check _apply_cpa_strategy_fields vs tests/wsdl_cache/campaigns.xml
  • Audit Notification / TimeTargeting validators
  • Audit parse_priority_goals_spec
  • Check incompatible-flag and required-field enforcement vs WSDL

I focused only on bugs, logical errors, and data-loss / API-rejection risks — not style.


1. PAY_FOR_CONVERSION_CRR produces guaranteed-broken payloads — required Crr field has no CLI flag (blocker)

WSDL (tests/wsdl_cache/campaigns.xml, StrategyPayForConversionCrrAdd):

<element name="Crr"    type="xsd:int"  minOccurs="1" maxOccurs="1"/>
<element name="GoalId" type="xsd:long" minOccurs="1" maxOccurs="1"/>

Both Crr and GoalId are required. The PR ships only --goal-id and explicitly tests this strategy:

# tests/test_dry_run.py:1697  (test_campaigns_add_pay_for_conversion_crr_search_payload)
assert search["PayForConversionCrr"] == {"GoalId": 555}

That payload has no Crr and will be rejected by the live API every time. The PR advertises and accepts --search-strategy PAY_FOR_CONVERSION_CRR but provides no way to actually populate the request. Either:

  • add --crr INT (with the same compatibility gating logic), or
  • drop PAY_FOR_CONVERSION_CRR from _SEARCH_STRATEGY_TO_WSDL_SUBTYPE and reject it at the CLI with a clear "not yet supported".

The current state — half-shipped — is worse than either: the surface looks usable, MCP/downstream callers will wire it up, and every call will fail at the API. Fix this →


2. CPA strategies silently emit incomplete subtype blocks when required fields are omitted

The PR description claims:

Strategy compatibility is enforced via UsageError at CLI level — invalid combinations never reach the API.

But the CLI does not enforce that required WSDL fields are actually present. Concrete cases that go through today:

Invocation What CLI sends WSDL says
--search-strategy AVERAGE_CPA (no --average-cpa, no --goal-id) Search: {BiddingStrategyType: "AVERAGE_CPA"}no AverageCpa subblock at all (_place only writes the block if any field landed in it, campaigns.py:198) AverageCpa and GoalId both minOccurs=1 on StrategyAverageCpaAdd
--search-strategy AVERAGE_CPA_MULTIPLE_GOALS (no --priority-goals) Search.AverageCpaMultipleGoals = {} and TextCampaign.PriorityGoals absent PriorityGoalsArray.Items is minOccurs=1 maxOccurs=unbounded once PriorityGoals is used; without PriorityGoals the multi-goal strategy is unusable
--search-strategy PAY_FOR_CONVERSION_MULTIPLE_GOALS (no --priority-goals) Same Same
--search-strategy AVERAGE_CPA_PER_CAMPAIGN / AVERAGE_CPA_PER_FILTER (any combo) See issue #3 below n/a

The "guarantee" in the PR description is false. The fix is one of:

  1. Mark _STRATEGY_REQUIRES_PRIORITY_GOALS subtypes as actually requiring --priority-goals (raise UsageError if absent).
  2. For AverageCpa / PayForConversionCrr, require --goal-id (and --average-cpa/--crr) when the strategy is chosen.
  3. Document these as INTERNAL_VALIDATION entries in tests/test_wsdl_parity_gate.py so the gate covers them.

This is the most likely real-world failure mode — a user picks the strategy, forgets one of the typed flags, and gets a confusing API error from the wire instead of a clear CLI message. Fix this →


3. _NETWORK_STRATEGY_TO_WSDL_SUBTYPE maps to subtypes that don't exist on TextCampaign / DynamicTextCampaign

direct_cli/commands/campaigns.py:36–40:

_NETWORK_STRATEGY_TO_WSDL_SUBTYPE = {
    "AVERAGE_CPA_PER_CAMPAIGN": "AverageCpaPerCampaign",
    "AVERAGE_CPA_PER_FILTER":   "AverageCpaPerFilter",
}

But TextCampaignStrategyAddBase and DynamicTextCampaignStrategyAddBase (the bases used by TextCampaignNetworkStrategyAdd / DynamicTextCampaignNetworkStrategyAdd) do not declare AverageCpaPerCampaign or AverageCpaPerFilter fields — those exist only on SmartCampaignStrategyAddBase. And the SMART_CAMPAIGN branch (campaigns.py:718) doesn't call _apply_cpa_strategy_fields.

So today: passing --type TEXT_CAMPAIGN --network-strategy AVERAGE_CPA_PER_CAMPAIGN --goal-id 5 --average-cpa 1000000 builds

"Network": {
  "BiddingStrategyType": "AVERAGE_CPA_PER_CAMPAIGN",
  "AverageCpaPerCampaign": {"AverageCpa": 1000000, "GoalId": 5}
}

— a key that isn't in the WSDL for TextCampaignNetworkStrategyAdd. Either:

  • delete these two entries from _NETWORK_STRATEGY_TO_WSDL_SUBTYPE (they only ever apply to SmartCampaign, which doesn't route here), or
  • if the intent was to support these for SmartCampaign network strategy, wire _apply_cpa_strategy_fields into the SMART_CAMPAIGN branch and gate accordingly. Fix this →

Looked at but not flagged

  • parse_priority_goals_spec (utils.py) — handles bad-shape, empty, non-int, trailing-comma, no-separator. Errors all surface as --priority-goals. ✓
  • _validate_notification_shape / _validate_time_targeting_shape — type-checked, empty rejected, unknown top-level / nested keys rejected. ✓
  • BidCeiling correctly excluded from StrategyPayForConversionCrrAdd and StrategyPayForConversionMultipleGoalsAdd (the two PR-noted WSDL audit catches). ✓
  • PriorityGoals placed on the TextCampaign / DynamicTextCampaign block, not on the strategy — matches TextCampaignAddItem.PriorityGoals in WSDL. ✓
  • --counter-ids accepted on Text/Dynamic, rejected for SMART_CAMPAIGN (singular --counter-id only). ✓
  • No secrets, injection vectors, or destructive paths introduced.

Summary

Three real issues, all in the strategy-construction code. Issue #1 (PAY_FOR_CONVERSION_CRR with no --crr) and issue #2 (incomplete CPA subblocks pass silently) directly contradict the PR's "invalid combinations never reach the API" promise — downstream MCP callers will hit API rejections that the CLI is supposed to prevent. Issue #3 is dead/misleading code that produces an invalid payload if a user trips into it.
• Branch: worktree-issue-204-campaigns-cpa-flags

CI quality job's `test_readme_direct_examples_are_single_line_canonical_commands`
rejects backslash line-continuations in README `direct ...` examples.
Refold the new CPA / multi-goal / Notification+TimeTargeting examples
into single lines (matching surrounding examples).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds new typed CLI flags to direct campaigns add to support CPA-oriented bidding strategies and campaign-level Notification / TimeTargeting blocks, aiming to prevent invalid combinations from reaching the API and to improve downstream automation compatibility.

Changes:

  • Added new campaigns add options (--goal-id, --priority-goals, --average-cpa, --bid-ceiling, --counter-ids, --notification, --time-targeting) and new payload composition/validation helpers.
  • Introduced parse_priority_goals_spec() in direct_cli/utils.py for goal_id:value,… parsing into WSDL PriorityGoalsItem[].
  • Expanded dry-run and WSDL-coverage payload fixtures/tests plus README + changelog documentation for the new surfaces.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
direct_cli/commands/campaigns.py Adds new Click flags, CPA-strategy payload injection, and JSON shape validators for Notification/TimeTargeting.
direct_cli/utils.py Adds parse_priority_goals_spec() helper for PriorityGoals parsing.
tests/test_dry_run.py Adds dry-run tests for new flags, payload shapes, and invalid combinations/inputs.
tests/api_coverage_payloads.py Adds new campaigns add payload cases for WSDL parity/coverage gate.
README.md Documents new CPA/Notification/TimeTargeting usage examples (EN + RU).
CHANGELOG.md Documents the new campaigns add typed flags and notes about issue/WSDL deviations.
Comments suppressed due to low confidence (4)

README.md:361

  • These new examples use shell line continuations (\) and multi-line commands, but this README section explicitly documents a contract that canonical examples must be single-line (and disallows line continuations). Please rewrite the added commands as single-line examples to match the documented CLI/README contract.

This issue also appears on line 1044 of the same file.

# CPA strategy (single goal): --goal-id required, --average-cpa / --bid-ceiling are micro-rubles
direct campaigns add --name "CPA Campaign" --start-date 2026-06-01 --type TEXT_CAMPAIGN --search-strategy AVERAGE_CPA --network-strategy SERVING_OFF --goal-id 1234567 --average-cpa 500000000 --bid-ceiling 1000000000 --counter-ids 111,222 --dry-run

# Multi-goal CPA via PriorityGoals (goal_id:value pairs, WSDL PriorityGoalsItem)
direct campaigns add --name "Multi-Goal CPA" --start-date 2026-06-01 --type TEXT_CAMPAIGN --search-strategy AVERAGE_CPA_MULTIPLE_GOALS --network-strategy SERVING_OFF --priority-goals 1234567:80,9876543:20 --bid-ceiling 1000000000 --dry-run

# Notification (Sms/Email) and TimeTargeting accept JSON with WSDL CamelCase keys
direct campaigns add --name "Notify+Schedule" --start-date 2026-06-01 --type TEXT_CAMPAIGN --search-strategy HIGHEST_POSITION --network-strategy SERVING_OFF --notification '{"EmailSettings":{"Email":"[email protected]","SendWarnings":"YES"}}' --time-targeting '{"Schedule":["1A0123456789ABCDEFGHIJKL"],"ConsiderWorkingWeekends":"YES"}' --dry-run

# Update / lifecycle
direct campaigns update --id 12345 --name "New Name" --status SUSPENDED --budget 100000000 --start-date 2024-02-10 --end-date 2024-03-01
direct campaigns suspend --id 12345
direct campaigns resume --id 12345
direct campaigns archive --id 12345
direct campaigns unarchive --id 12345
direct campaigns delete --id 12345
**README.md:1061**
* Same as the English section: these added Russian examples use shell line continuations (`\`) and multi-line commands, but the README’s documented contract says canonical examples must be single-line and must not use line continuations. Please rewrite these examples as single-line commands.

direct campaigns update --id 12345 --name "Новое название" --status SUSPENDED --budget 100000000 --start-date 2024-02-10 --end-date 2024-03-01
direct campaigns suspend --id 12345
direct campaigns resume --id 12345
direct campaigns archive --id 12345
direct campaigns unarchive --id 12345
direct campaigns delete --id 12345


#### Группы объявлений

```bash
direct adgroups get --campaign-ids 1,2,3 --limit 50
direct adgroups add --name "Группа 1" --campaign-id 12345 --region-ids 1,225 --dry-run
direct adgroups add --name "Динамическая группа" --campaign-id 12345 --type DYNAMIC_TEXT_AD_GROUP --region-ids 1,225 --domain-url example.com --dry-run
direct adgroups add --name "Смарт-группа" --campaign-id 12345 --type SMART_AD_GROUP --region-ids 1,225 --feed-id 170 --ad-title-source FEED_NAME --ad-body-source FEED_NAME --dry-run
direct adgroups update --id 67890 --name "Новое название" --status SUSPENDED --region-ids 1,225
direct adgroups delete --id 67890
**README.md:361**
* The README’s “Documentation Contract” section says invalid examples include command lines that pass raw JSON flags, but the added `--notification` / `--time-targeting` examples embed raw JSON strings. Either adjust that contract text (to allow these specific JSON-typed flags) or change the examples to avoid raw JSON in canonical snippets.

This issue also appears on line 1056 of the same file.

direct campaigns resume --id 12345
direct campaigns archive --id 12345
direct campaigns unarchive --id 12345
direct campaigns delete --id 12345


README.md:1061

  • Same issue in the Russian examples: the README contract states raw JSON flags are invalid in canonical snippets, but the new --notification / --time-targeting example includes inline JSON strings. Consider updating the contract text or changing the example format.
direct adgroups add --name "Группа 1" --campaign-id 12345 --region-ids 1,225 --dry-run
direct adgroups add --name "Динамическая группа" --campaign-id 12345 --type DYNAMIC_TEXT_AD_GROUP --region-ids 1,225 --domain-url example.com --dry-run
direct adgroups add --name "Смарт-группа" --campaign-id 12345 --type SMART_AD_GROUP --region-ids 1,225 --feed-id 170 --ad-title-source FEED_NAME --ad-body-source FEED_NAME --dry-run
direct adgroups update --id 67890 --name "Новое название" --status SUSPENDED --region-ids 1,225
direct adgroups delete --id 67890
</details>

Comment thread direct_cli/utils.py
Comment on lines +202 to +203
if not value:
return None
Comment on lines +229 to +255
def _validate_time_targeting_shape(time_targeting: object) -> None:
"""Reject malformed --time-targeting JSON before sending to API."""
if not isinstance(time_targeting, dict) or not time_targeting:
raise click.UsageError(
"--time-targeting must be a non-empty JSON object with "
f"keys from {sorted(_TIME_TARGETING_KEYS)}"
)
unknown = set(time_targeting) - _TIME_TARGETING_KEYS
if unknown:
raise click.UsageError(
f"--time-targeting contains unknown key(s) {sorted(unknown)}; "
f"allowed: {sorted(_TIME_TARGETING_KEYS)}"
)
holidays = time_targeting.get("HolidaysSchedule")
if holidays is not None:
if not isinstance(holidays, dict):
raise click.UsageError(
"--time-targeting.HolidaysSchedule must be a JSON object"
)
unknown_h = set(holidays) - _HOLIDAYS_SCHEDULE_KEYS
if unknown_h:
raise click.UsageError(
"--time-targeting.HolidaysSchedule contains unknown "
f"key(s) {sorted(unknown_h)}; allowed: "
f"{sorted(_HOLIDAYS_SCHEDULE_KEYS)}"
)

Comment on lines +24 to +61
# WSDL: BiddingStrategyType enum value → Strategy*Add subtype field name
# in TextCampaignSearch/Network/SmartCampaign… containers.
# Only CPA-shaped subtypes that accept --average-cpa / --goal-id /
# --bid-ceiling / --priority-goals are listed; legacy types
# (HIGHEST_POSITION etc.) do not carry these fields and must not get
# a nested subtype block at all.
_SEARCH_STRATEGY_TO_WSDL_SUBTYPE = {
"AVERAGE_CPA": "AverageCpa",
"PAY_FOR_CONVERSION_CRR": "PayForConversionCrr",
"AVERAGE_CPA_MULTIPLE_GOALS": "AverageCpaMultipleGoals",
"PAY_FOR_CONVERSION_MULTIPLE_GOALS": "PayForConversionMultipleGoals",
}
_NETWORK_STRATEGY_TO_WSDL_SUBTYPE = {
# Smart-only subtype lives on Network side as well.
"AVERAGE_CPA_PER_CAMPAIGN": "AverageCpaPerCampaign",
"AVERAGE_CPA_PER_FILTER": "AverageCpaPerFilter",
}

_STRATEGY_SUPPORTS_AVERAGE_CPA = {
"AverageCpa",
"AverageCpaPerCampaign",
}
_STRATEGY_SUPPORTS_GOAL_ID = {
"AverageCpa",
"PayForConversionCrr",
"AverageCpaPerCampaign",
"AverageCpaPerFilter",
}
_STRATEGY_SUPPORTS_BID_CEILING = {
"AverageCpa",
"AverageCpaPerCampaign",
"AverageCpaPerFilter",
"AverageCpaMultipleGoals",
}
_STRATEGY_REQUIRES_PRIORITY_GOALS = {
"AverageCpaMultipleGoals",
"PayForConversionMultipleGoals",
}
Comment on lines +156 to +168
# Single-goal CPA strategies must reject --priority-goals;
# only *_MULTIPLE_GOALS subtypes carry PriorityGoals.
if priority_goals_items is not None:
chosen_subtype = search_subtype or network_subtype
if chosen_subtype not in _STRATEGY_REQUIRES_PRIORITY_GOALS:
raise click.UsageError(
"--priority-goals is only valid with "
"AVERAGE_CPA_MULTIPLE_GOALS / "
"PAY_FOR_CONVERSION_MULTIPLE_GOALS strategies; "
f"got --search-strategy={search_strategy!r}, "
f"--network-strategy={network_strategy!r}"
)
sub_campaign_block["PriorityGoals"] = {"Items": priority_goals_items}
f"got --search-strategy={search_strategy!r}, "
f"--network-strategy={network_strategy!r}"
)

…network map

Address all three @claude review findings on PR #219:

1. PAY_FOR_CONVERSION_CRR was advertising itself but had no way to
   populate the WSDL-required Crr field. Add --crr (xsd:int) with
   gating: rejected outside PAY_FOR_CONVERSION_CRR.

2. Add CLI-level minOccurs=1 enforcement via
   _STRATEGY_REQUIRED_TYPED_FLAGS. Picking a CPA strategy without
   the required typed flags now raises UsageError up-front instead
   of silently sending a half-configured payload that the API would
   reject. Affects AVERAGE_CPA (needs --average-cpa + --goal-id),
   PAY_FOR_CONVERSION_CRR (needs --crr + --goal-id), and the two
   *_MULTIPLE_GOALS strategies (need --priority-goals).

3. Empty _NETWORK_STRATEGY_TO_WSDL_SUBTYPE: AverageCpaPerCampaign
   and AverageCpaPerFilter live only on SmartCampaignStrategyAddBase,
   not on Text/Dynamic network strategy. Keeping them in the network
   map for TEXT_CAMPAIGN/DYNAMIC_TEXT_CAMPAIGN would silently emit
   WSDL-invalid keys.

Adds 6 negative regression tests and updates the existing
test_campaigns_add_pay_for_conversion_crr_search_payload to pass
--crr alongside --goal-id.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@axisrow

axisrow commented May 21, 2026

Copy link
Copy Markdown
Owner Author

@claude review. All three FIX findings addressed in 9ca8852: added --crr flag for PAY_FOR_CONVERSION_CRR (WSDL Crr minOccurs=1), added CLI-level minOccurs=1 enforcement via _STRATEGY_REQUIRED_TYPED_FLAGS for all CPA subtypes, and emptied _NETWORK_STRATEGY_TO_WSDL_SUBTYPE so Per-Campaign/Per-Filter (Smart-only in WSDL) can no longer leak into Text/Dynamic network strategy. 6 new negative regression tests cover each gap. Focus on critical issues only.

@claude

claude Bot commented May 21, 2026

Copy link
Copy Markdown

Claude finished @axisrow's task in 3m 36s —— View job


Claude PR Review — Verifying fix commit 9ca8852

  • Inspect commit 9ca8852 vs. prior review findings
  • Verify --crr flag implementation against WSDL StrategyPayForConversionCrrAdd
  • Verify _STRATEGY_REQUIRED_TYPED_FLAGS enforces minOccurs=1 for all CPA subtypes
  • Verify _NETWORK_STRATEGY_TO_WSDL_SUBTYPE no longer maps Per-Campaign / Per-Filter
  • Review the 6 new negative regression tests
  • Look for any new critical issues introduced

All three prior FIX findings are properly resolved. No new critical issues. Detail below; per request, style / doc-drift nits are not flagged.


Verification of prior findings

✅ Finding #1PAY_FOR_CONVERSION_CRR missing required Crr

direct_cli/commands/campaigns.py:574-579 adds --crr as type=int (correct: WSDL StrategyPayForConversionCrrAdd.Crr is xsd:int, not micro-rubles). Gating at campaigns.py:226-233 (_STRATEGY_SUPPORTS_CRR = {"PayForConversionCrr"}) raises UsageError if used on any other subtype. Placement at block["Crr"] = crr lands inside the PayForConversionCrr subtype block. The updated positive test tests/test_dry_run.py:1697 now asserts both fields: {"Crr": 8, "GoalId": 555}. Negative coverage:

  • test_campaigns_add_rejects_crr_for_average_cpa (tests/test_dry_run.py:2087) — --crr on AVERAGE_CPA rejected.
  • test_campaigns_add_rejects_pay_for_conversion_crr_without_crr (tests/test_dry_run.py:2034) — bare CRR strategy rejected.

✅ Finding #2 — Half-populated CPA subblocks reaching the API

_STRATEGY_REQUIRED_TYPED_FLAGS (campaigns.py:65-71) is consulted by _ensure_required (campaigns.py:189-212), which runs before _place / _place_multiple_goals. The check fires when the chosen subtype is set and the corresponding provided_lookup value is None. Tested for all four required-field paths:

  • AVERAGE_CPA without --average-cpa or --goal-id → both flags reported missing (test_campaigns_add_rejects_average_cpa_strategy_without_required_fields, tests/test_dry_run.py:2061).
  • PAY_FOR_CONVERSION_CRR without --crr and without --goal-id (two distinct tests).
  • AVERAGE_CPA_MULTIPLE_GOALS without --priority-goals (tests/test_dry_run.py:2075).

The Strategy{subtype}Add minOccurs=1 error message text correctly cites the WSDL invariant, which is what the PR description promises.

✅ Finding #3 — Smart-only subtypes leaking into Text/Dynamic network strategy

_NETWORK_STRATEGY_TO_WSDL_SUBTYPE (campaigns.py:36-41) is now Dict[str, str] = {} with a comment explaining why the Per-Campaign / Per-Filter entries were removed. Both subtypes were also dropped from _STRATEGY_SUPPORTS_AVERAGE_CPA, _STRATEGY_SUPPORTS_GOAL_ID, and _STRATEGY_SUPPORTS_BID_CEILING, so they're fully unreachable. test_campaigns_add_rejects_text_campaign_with_per_campaign_network_strategy (tests/test_dry_run.py:2105) confirms the rejection path via the "CPA-shaped" upfront check.

I confirmed against tests/wsdl_cache/campaigns.xml:1581 that TextCampaignStrategyAddBase / DynamicTextCampaignStrategyAddBase indeed do not declare AverageCpaPerCampaign or AverageCpaPerFilter — they exist only on SmartCampaignStrategyAddBase, and the SMART_CAMPAIGN branch (campaigns.py:783-818) does not call _apply_cpa_strategy_fields. Drop is correct.


Sanity-checked other risks

  • Field-loss when subtype is None: _place(..., subtype=None) returns early before touching bidding_strategy[side], so the empty network map cannot accidentally drop typed flags silently — the upfront has_cpa_flags and search_subtype is None and network_subtype is None → UsageError catches that case (campaigns.py:158-168).
  • WSDL parity gate: the existing tests/test_wsdl_parity_gate.py STRATEGY_TYPES invariant still covers AverageCpa / PayForConversionCrr / etc.; the dropped Per-* entries never appeared in the search-side _SEARCH_STRATEGY_TO_WSDL_SUBTYPE, so the gate is unaffected.
  • No regression on existing single-goal / multi-goal payloads: the only behavioral change for valid invocations is the new requirement to pass --crr for PAY_FOR_CONVERSION_CRR, which the corresponding positive test was updated for.
  • No secrets / injection / destructive surfaces introduced.

Summary

LGTM on the fix commit — the three blockers from the prior review are closed cleanly, the new minOccurs=1 enforcement matches the WSDL invariants in tests/wsdl_cache/campaigns.xml, and the negative-path tests pin the new behavior. The PR's "invalid combinations never reach the API" claim now actually holds for the surfaced strategies.
• Branch: worktree-issue-204-campaigns-cpa-flags

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

campaigns add: typed flags for Goals (CPA strategy), Notification, NetworkSettings, TimeTargeting

2 participants