Skip to content

feat(media-buy): reject get_products products without pricing_options#2269

Open
EmmaLouise2018 wants to merge 1 commit into
mainfrom
EmmaLouise2018/pattaya-v2
Open

feat(media-buy): reject get_products products without pricing_options#2269
EmmaLouise2018 wants to merge 1 commit into
mainfrom
EmmaLouise2018/pattaya-v2

Conversation

@EmmaLouise2018

Copy link
Copy Markdown
Contributor

Summary

pricing_options is a required, minItems: 1 field in AdCP 3.1 — a product that advertises no pricing model is non-transactable. The client now drops such products from completed get_products responses before callers and completion handlers see them.

  • New enforceProductPricingOptions() wired in at the top of applyProductPropertyPolicy() — the universal completion chokepoint — so it covers all five paths (sync executeAndHandle, polling waitForCompletion, track, webhook, and the second executeTask path) with one integration point, and fires even when no property policy is configured.
  • Runs independent of the response validation mode, so unpriced products are removed even under responses: 'warn' | 'off'.
  • Mutates the result in place (matching the existing property-policy filter path) to preserve the non-enumerable match accessor.
  • Outcome recorded in result.metadata.productPricingPolicy and a product_missing_pricing_options debug-log notice; the webhook path forwards the same summary on WebhookMetadata.

Config: default-on; opt out with validation.rejectProductsWithoutPricingOptions: false.

Why default-on

pricing_options is spec-required (minItems: 1), so a compliant seller never trips this. Returning an unpriced product to a buyer is a correctness bug. Bumped minor, consistent with how the repo treats spec-conformance tightening (cf. fix(compliance): emit stripped field notices). Opt-out is provided for callers deliberately inspecting malformed seller responses.

Changes

  • src/lib/core/SingleAgentClient.tsenforceProductPricingOptions(), config option, helpers, wiring.
  • src/lib/core/ConversationTypes.ts / src/lib/core/AsyncHandler.tsproductPricingPolicy on TaskResultMetadata and WebhookMetadata.
  • test/lib/product-pricing-options-rejection.test.js — new: default drop, empty-array = missing, opt-out, all-priced no-op, webhook path.
  • Updated 3 fixture files whose products were spec-invalid (missing pricing_options).

Testing

  • npm run typecheck — clean
  • npm run lint — 0 errors
  • Full fast suite — 12034 pass, 0 fail
  • Changeset added (minor)

pricing_options is a required, minItems:1 field in AdCP 3.1 — a product
that advertises no pricing model is non-transactable. The client now drops
such products from completed get_products responses before callers and
completion handlers see them, on every completion path (sync, polling,
track, webhook) and independent of the response validation mode.

Recorded in result.metadata.productPricingPolicy and a
product_missing_pricing_options debug-log notice. Default-on; opt out via
validation.rejectProductsWithoutPricingOptions: false.

@aao-ipr-bot aao-ipr-bot Bot 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.

Approving. Buyer-side enforcement of a spec-required field, wired at the one completion chokepoint so all five paths inherit it — right shape, and the disclosure (metadata + typed debug notice + opt-out) keeps it a witness, not a translator.

Premise checks out. ad-tech-protocol-expert confirmed pricing_options is in the Product schema's top-level required array and carries minItems: 1, identically in AdCP 3.0.0 and 3.1.0 (adcontextprotocol/adcp core/product.json on main and the v3.0.0 tag; corroborated by core.generated.ts:3748). So treating [] as equivalent to missing is spec-correct, and the cross-version worry is a non-issue: a 3.0 seller is held to the same constraint, and the v2.5 legacy layer types the field as required too. No supported wire version emits a spec-valid unpriced product this filter would wrongly drop.

Things I checked

  • Chokepoint claim is true. enforceProductPricingOptions is the first statement in applyProductPropertyPolicy, and every completion path routes through it: sync executeAndHandle (SingleAgentClient.ts:1696), polling waitForCompletion (1956), trackapplyProductPropertyPolicyToTaskInfo (1941→1979), webhook (2031), second executeTask (3101). Fires even with no property policy configured.
  • match accessor survives the in-place mutation. attachMatch defines match non-enumerably on the TaskResult object, not on result.data; reassigning result.data to a spread clone while keeping result identity matches the existing property-policy filter path. Comment's claim is accurate.
  • Cast isn't trusted. as unknown as GetProductsResponse is immediately re-read via (response as { products?: unknown }).products and guarded with Array.isArray(...) + length checks. products: undefined, empty array, all-unpriced, and all-priced are each handled and tested.
  • Webhook metadata forwarding is correct. productPricingPolicy is spread onto WebhookMetadata right beside productPropertyPolicy in applyProductPropertyPolicyToWebhookResult; the webhook test asserts handlerCalls[0].metadata.productPricingPolicy.rejected_count.
  • Changeset present (reject-products-without-pricing-options.md, minor); fixture updates add pricing_options to four previously spec-invalid product fixtures — necessary to keep them transactable, not masking. code-reviewer: no Blocker/High/Medium.

Follow-ups (non-blocking — file as issues)

  • Reconcile the default with filterInvalidProducts. This is the second product-dropping toggle in the validation struct, default-on, two lines below one (filterInvalidProducts, SingleAgentClient.ts:464-476) that defaults off. Both ad-tech-protocol-expert and dx-expert flagged the asymmetry: an adopter who reads the config learns the SDK's existing product-dropping behavior is opt-in, then gets a second one that isn't. Default-on is defensible here (the field is spec-required, the product is non-transactable) — but pick one discipline for buyer-side spec-conformance dropping and a one-line cross-reference in the filterInvalidProducts JSDoc kills the surprise.
  • Changeset under-discloses the upgrade impact. The prose explains the mechanism thoroughly but never says the load-bearing sentence: adopters talking to non-strict/noncompliant sellers may receive fewer products after this minor bump. Compliant sellers are unaffected; the slice that isn't gets a silent count drop with only a type: 'warning' debug log and a metadata field as breadcrumbs — passively discoverable, found only if you already suspect products went missing. Add the upgrade note + pointer to result.metadata.productPricingPolicy. minor is borderline-but-acceptable with that sentence.
  • Track path filters but doesn't disclose. applyProductPropertyPolicyToTaskInfo (SingleAgentClient.ts:1999-2000) drops the unpriced products via policyResult.data, but TaskInfo has no metadata/debug slot, so productPricingPolicy and the notice are discarded on this one path. Pre-existing and consistent — productPropertyPolicy is dropped the same way — so the "all five paths" claim is true for filtering, not for disclosure. Worth one line in the PR body so it isn't read as full coverage.

Minor nits (non-blocking)

  1. Mode-independence inside a validation block reads slightly off. rejectProductsWithoutPricingOptions lives under validation but the JSDoc has to clarify it ignores validation.responses. Shared with its neighbors, so leave it — but if validation keeps accreting mode-independent product filters, a future productFilters sub-struct would read better.

LGTM. Follow-ups noted.

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.

1 participant