From 6573e9c2ee203eb3f45153a48c6286c8a3ea8914 Mon Sep 17 00:00:00 2001 From: Aleksander Sekowski Date: Sat, 24 Oct 2026 10:46:40 -0700 Subject: [PATCH 1/3] Add discussion fixtures for pricing alternatives and seller fees --- .github/workflows/validate.yml | 10 + .../pricing-commitment-arithmetic.md | 158 ++ fixtures/pricing/cases.json | 1450 +++++++++++++++++ scripts/check_pricing_fixtures.py | 166 ++ 4 files changed, 1784 insertions(+) create mode 100644 docs/proposals/pricing-commitment-arithmetic.md create mode 100644 fixtures/pricing/cases.json create mode 100644 scripts/check_pricing_fixtures.py diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 1ea9da9..7ca1a1c 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -67,3 +67,13 @@ jobs: python-version: "3.12" - name: Check every field in the body appears in Appendix A run: python3 scripts/check_appendix.py + + pricing-fixtures: + name: Pricing discussion fixtures + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: python3 scripts/check_pricing_fixtures.py diff --git a/docs/proposals/pricing-commitment-arithmetic.md b/docs/proposals/pricing-commitment-arithmetic.md new file mode 100644 index 0000000..db0aa6a --- /dev/null +++ b/docs/proposals/pricing-commitment-arithmetic.md @@ -0,0 +1,158 @@ +# Resolve one price and account for seller fees once + +Implementation feedback for [#30](https://github.com/IABTechLab/OpenProposal/issues/30), +[#94](https://github.com/IABTechLab/OpenProposal/issues/94) and +[#115](https://github.com/IABTechLab/OpenProposal/issues/115), against draft +`44dfaaf618736219426c311ec997b6d37e94b950`. + +This is a proposed calculation profile with executable discussion vectors. +It is not an adopted requirement, an OpenProposal schema, or evidence of a +defect in a deployed implementation. Its fixture keys are an adapter format. + +## The proposals currently produce incompatible results + +#94 makes `pricing[]` entries alternatives and selects exactly one. +#115 puts seller-billed fees into additional `pricing[]` entries and includes +them in `commitment.amount`. Treating a fee as another alternative does not +add it to the selected price. Summing all rows instead loses #94's choice of +cost method, currency and transaction mechanism. + +There is also an arithmetic consequence. At USD 25 CPM, a USD 50,000 spend +commitment containing a USD 2,500 production fee leaves USD 47,500 for media: + +``` +media_spend = 50,000 - 2,500 = 47,500 USD +derived_units = 47,500 / 25 * 1,000 = 1,900,000 impressions +``` + +Computing units from the whole commitment gives 2,000,000 impressions. Those +units cost USD 50,000 before the fee, for a seller total of USD 52,500. +Therefore #115's statement that `derived_units` is unaffected needs revision +if `amount` includes charges not included in the unit rate. + +## Proposed wording for joint resolution + +This wording assumes adoption of the alternative-selection part of #94. +It supplies the missing fee basis in #115 and clarifies the counter described +in #30. The working group can decide these pieces together without adopting +the entire cost-method vocabulary. + +### Sections 3.1 and 5.6: select the price and compose the rate + +`pricing[]` entries are alternative prices, not additive charges. A commitment +MUST resolve to exactly one entry under the selection rule in #94. Only that +entry determines the cost method, rate, floor and currency. An implementation +MUST NOT sum alternative entries to compute the buyer's charge. + +A selected option's `delta_flat` is a one-time seller charge for that line +item commitment. It MUST NOT enter the unit rate or be represented as another +alternative price solely to make it additive. A cost already included in the +unit rate MUST NOT also be added as a flat charge. Each selected option that +is not already included contributes its charge once. + +Unless a final-rate counter has been accepted, the composed rate is the chosen +entry's `gross_rate`, plus applicable `delta_cpm` values, then multiplied by +each applicable `(1 + delta_pct)` factor. For this proposal, `delta_pct` is a +decimal fraction: `0.20` means a 20 percent increase. `delta_cpm` applies only +to CPM and CPMV. Flat charges are outside this calculation. + +An accepted counter stated as the final composed rate MUST become +`agreed_rate` without reapplying the option rate adjustments. It MUST be bound +to the proposal revision, chosen price and resolved selections to which the +seller assented. A changed selection or revision requires a new applicable +acceptance. This rule does not prescribe an acceptance protocol. The floor +comparison uses that same final composed rate. A final-rate counter does not +waive separately identified flat charges unless the accepted terms explicitly +change those charges. + +### Section 5.6.1: define the spend basis + +For `basis = spend`, `amount` is the total pre-tax amount payable to the seller +for this line item, including applicable seller flat charges. It excludes +amounts payable by the buyer to other parties. When the selected cost method +has a unit rate, the amount available for those units is: + +``` +flat_charges = sum of applicable one-time seller charges +media_spend = commitment.amount - flat_charges +derived_units = media_spend / agreed_rate * rate_per +``` + +`rate_per` is 1,000 for CPM and CPMV, and 1 for CPC. The agreed rate MUST be +positive. Flat charges MUST NOT exceed the spend commitment. Zero remaining +media spend yields zero informative units and still has to satisfy any +applicable minimum-unit requirement. + +For `basis = units`, the committed units remain binding and the corresponding +seller total is: + +``` +media_spend = commitment.amount / rate_per * agreed_rate +seller_total = media_spend + flat_charges +``` + +`min_spend` and budget bounds apply to the seller total on the same pre-tax +basis. Impression bounds apply only when the quantity is actually measured +in impressions; clicks or viewable impressions MUST NOT silently be treated +as the same quantity. The selected entry's currency applies to the rate, +flat charges and budget bounds. No currency conversion is implied. + +The calculations above do not round informative quantities. A displayed +rounded estimate MUST NOT change the committed amount or imply additional +authorized spend. Invoice rounding and minor-unit settlement need a separate +rule if they are to be interoperable. + +### Deliberate limits of this first proposal + +- This profile addresses seller fees already expressible through selected + `delta_flat` options. It does not invent a general fee object. If #115 needs + mandatory charges that cannot use that structure, their representation + needs a separate decision. They cannot be silently omitted. +- The proposed `billing.tax_inclusive` branch of #115 needs a tax amount or a + deterministic tax calculation before its units can be derived. The runner + rejects that branch rather than assuming tax is zero. +- #103's print discounts and surcharges introduce separate calculation bases. + Its `value_pct` and conditions are not interpreted by this profile. +- Cost methods other than CPM, CPMV and CPC are not implemented here. An + unsupported profile case is not a declaration that an OpenProposal is + invalid. Extension definitions and transaction-mechanism eligibility stay + with #94. +- A passing calculation establishes arithmetic consistency only. It does + not establish seller acceptance, inventory reservation or an agreed order. + +## Worked counter example + +A base CPM of USD 30 plus USD 4, USD 1.50 and USD 3 option adjustments gives +USD 38.50. If the seller accepts USD 30 as the final rate for those selections, +the rate is USD 30. Applying the adjustments again incorrectly restores +USD 38.50. A separate USD 2,500 flat fee remains outside that rate: one million +committed impressions cost USD 32,500 in total. + +## Reproduce the vectors + +``` +python3 scripts/check_pricing_fixtures.py +python3 scripts/check_pricing_fixtures.py --json +``` + +The corpus is [`fixtures/pricing/cases.json`](../../fixtures/pricing/cases.json). +Expected outcomes are fixed independently of the calculator. Cases cover +alternative selection, fee-inclusive spend, unit commitments, counters, +currency, floors, bounds, percentage composition and unsupported semantics. + +Adapter inputs contain only the already resolved selected adjustments that +are not included in the base price. They do not extract selections from a +proposal or implement #22. Adjustment IDs are field-qualified to avoid +counting the same resolved adjustment twice. The caller has already confirmed +mechanism eligibility and seller acceptance of any `accepted_counter`. +`selection_key` is an opaque test binding, not an OpenProposal field or an +implementation of revision authentication. + +Decimal strings and exact rational outputs avoid hidden binary floating point +rounding. For example, `"1000/3"` means the exact informative quantity one +thousand divided by three. This is a fixture convention, not a proposed JSON +wire type. Currency checks cover agreement and shape, not ISO-4217 membership. + +No normative body or field index is changed by this discussion corpus. If +the wording is adopted, update sections 3.1, 5.6 and 5.6.1, the affected field +descriptions in Appendices A and B, and the committed examples together. diff --git a/fixtures/pricing/cases.json b/fixtures/pricing/cases.json new file mode 100644 index 0000000..0c65d9e --- /dev/null +++ b/fixtures/pricing/cases.json @@ -0,0 +1,1450 @@ +{ + "profile": "pricing-discussion-1", + "cases": [ + { + "id": "cpm-no-fees", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [] + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "agreed_rate": "25", + "flat_charges": "0", + "media_spend": "50000", + "seller_total": "50000", + "units": "2000000" + } + } + }, + { + "id": "seller-fee-inside-spend", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [ + { + "id": "production/build", + "delta_flat": "2500" + } + ] + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "agreed_rate": "25", + "flat_charges": "2500", + "media_spend": "47500", + "seller_total": "50000", + "units": "1900000" + } + } + }, + { + "id": "units-add-fee-to-total", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "units", + "amount": "2000000", + "currency": "USD", + "adjustments": [ + { + "id": "production/build", + "delta_flat": "2500" + } + ] + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "agreed_rate": "25", + "flat_charges": "2500", + "media_spend": "50000", + "seller_total": "52500", + "units": "2000000" + } + } + }, + { + "id": "units-total-exceeds-budget", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "units", + "amount": "2000000", + "currency": "USD", + "adjustments": [ + { + "id": "production/build", + "delta_flat": "2500" + } + ], + "bounds": { + "max_budget": "50000" + } + }, + "expected": { + "error": "outside_max_budget" + } + }, + { + "id": "spend-fee-does-not-increase-total", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [ + { + "id": "production/build", + "delta_flat": "2500" + } + ], + "bounds": { + "max_budget": "50000", + "min_budget": "50000", + "min_spend": "50000" + } + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "agreed_rate": "25", + "flat_charges": "2500", + "media_spend": "47500", + "seller_total": "50000", + "units": "1900000" + } + } + }, + { + "id": "fees-exceed-spend", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "2499", + "currency": "USD", + "adjustments": [ + { + "id": "production/build", + "delta_flat": "2500" + } + ] + }, + "expected": { + "error": "fees_exceed_spend" + } + }, + { + "id": "fees-equal-spend-zero-media", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "2500", + "currency": "USD", + "adjustments": [ + { + "id": "production/build", + "delta_flat": "2500" + } + ] + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "agreed_rate": "25", + "flat_charges": "2500", + "media_spend": "0", + "seller_total": "2500", + "units": "0" + } + } + }, + { + "id": "two-fees-count-once", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [ + { + "id": "production/build", + "delta_flat": "2500" + }, + { + "id": "data/package", + "delta_flat": "500" + } + ] + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "agreed_rate": "25", + "flat_charges": "3000", + "media_spend": "47000", + "seller_total": "50000", + "units": "1880000" + } + } + }, + { + "id": "duplicate-adjustment-rejected", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [ + { + "id": "production/build", + "delta_flat": "2500" + }, + { + "id": "production/build", + "delta_flat": "2500" + } + ] + }, + "expected": { + "error": "duplicate_adjustment" + } + }, + { + "id": "flat-credit-out-of-scope", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [ + { + "id": "rebate", + "delta_flat": "-1" + } + ] + }, + "expected": { + "error": "unsupported_flat_credit" + } + }, + { + "id": "three-rate-deltas", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "30", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "38500", + "currency": "USD", + "adjustments": [ + { + "id": "audiences/a", + "delta_cpm": "4" + }, + { + "id": "audiences/b", + "delta_cpm": "1.5" + }, + { + "id": "formats/30s", + "delta_cpm": "3" + } + ] + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "agreed_rate": "77/2", + "flat_charges": "0", + "media_spend": "38500", + "seller_total": "38500", + "units": "1000000" + } + } + }, + { + "id": "accepted-final-counter-no-double-add", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "30", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "30000", + "currency": "USD", + "adjustments": [ + { + "id": "audiences/a", + "delta_cpm": "4" + }, + { + "id": "audiences/b", + "delta_cpm": "1.5" + }, + { + "id": "formats/30s", + "delta_cpm": "3" + } + ], + "selection_key": "revision-7/cpm-usd/options-abc", + "accepted_counter": { + "rate": "30", + "selection_key": "revision-7/cpm-usd/options-abc" + } + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "agreed_rate": "30", + "flat_charges": "0", + "media_spend": "30000", + "seller_total": "30000", + "units": "1000000" + } + } + }, + { + "id": "counter-keeps-flat-fees", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "30", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "32500", + "currency": "USD", + "adjustments": [ + { + "id": "audiences/a", + "delta_cpm": "4" + }, + { + "id": "audiences/b", + "delta_cpm": "1.5" + }, + { + "id": "formats/30s", + "delta_cpm": "3" + }, + { + "id": "production/build", + "delta_flat": "2500" + } + ], + "selection_key": "revision-7/cpm-usd/options-abc", + "accepted_counter": { + "rate": "30", + "selection_key": "revision-7/cpm-usd/options-abc" + } + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "agreed_rate": "30", + "flat_charges": "2500", + "media_spend": "30000", + "seller_total": "32500", + "units": "1000000" + } + } + }, + { + "id": "counter-on-changed-selection", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "30", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "30000", + "currency": "USD", + "adjustments": [ + { + "id": "audiences/a", + "delta_cpm": "4" + }, + { + "id": "audiences/b", + "delta_cpm": "1.5" + }, + { + "id": "formats/30s", + "delta_cpm": "3" + } + ], + "selection_key": "revision-7/cpm-usd/options-abcd", + "accepted_counter": { + "rate": "30", + "selection_key": "revision-7/cpm-usd/options-abc" + } + }, + "expected": { + "error": "stale_counter" + } + }, + { + "id": "counter-without-binding", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [], + "accepted_counter": { + "rate": "30", + "selection_key": "revision-7/cpm-usd/options-abc" + } + }, + "expected": { + "error": "counter_binding_required" + } + }, + { + "id": "counter-below-final-floor", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "30", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "30000", + "currency": "USD", + "adjustments": [ + { + "id": "audiences/a", + "delta_cpm": "4" + }, + { + "id": "audiences/b", + "delta_cpm": "1.5" + }, + { + "id": "formats/30s", + "delta_cpm": "3" + } + ], + "selection_key": "revision-7/cpm-usd/options-abc", + "accepted_counter": { + "rate": "19", + "selection_key": "revision-7/cpm-usd/options-abc" + } + }, + "expected": { + "error": "below_floor" + } + }, + { + "id": "floor-equality-allowed", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "30", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "30000", + "currency": "USD", + "adjustments": [ + { + "id": "audiences/a", + "delta_cpm": "4" + }, + { + "id": "audiences/b", + "delta_cpm": "1.5" + }, + { + "id": "formats/30s", + "delta_cpm": "3" + } + ], + "selection_key": "revision-7/cpm-usd/options-abc", + "accepted_counter": { + "rate": "20", + "selection_key": "revision-7/cpm-usd/options-abc" + } + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "agreed_rate": "20", + "flat_charges": "0", + "media_spend": "30000", + "seller_total": "30000", + "units": "1500000" + } + } + }, + { + "id": "percent-after-cpm-delta", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "36000", + "currency": "USD", + "adjustments": [ + { + "id": "format", + "delta_cpm": "5" + }, + { + "id": "rights", + "delta_pct": "0.20" + } + ] + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "agreed_rate": "36", + "flat_charges": "0", + "media_spend": "36000", + "seller_total": "36000", + "units": "1000000" + } + } + }, + { + "id": "percentage-compounds-and-excludes-flat", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "29500", + "currency": "USD", + "adjustments": [ + { + "id": "rights", + "delta_pct": "0.20" + }, + { + "id": "discount", + "delta_pct": "-0.10" + }, + { + "id": "production/build", + "delta_flat": "2500" + } + ] + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "agreed_rate": "27", + "flat_charges": "2500", + "media_spend": "27000", + "seller_total": "29500", + "units": "1000000" + } + } + }, + { + "id": "zero-percentage-factor", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [ + { + "id": "discount", + "delta_pct": "-1" + } + ] + }, + "expected": { + "error": "nonpositive_percentage_factor" + } + }, + { + "id": "percent-not-reapplied-to-counter", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "30", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "30000", + "currency": "USD", + "adjustments": [ + { + "id": "audiences/a", + "delta_cpm": "4" + }, + { + "id": "audiences/b", + "delta_cpm": "1.5" + }, + { + "id": "formats/30s", + "delta_cpm": "3" + }, + { + "id": "rights", + "delta_pct": "0.20" + } + ], + "selection_key": "revision-7/cpm-usd/options-abc", + "accepted_counter": { + "rate": "30", + "selection_key": "revision-7/cpm-usd/options-abc" + } + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "agreed_rate": "30", + "flat_charges": "0", + "media_spend": "30000", + "seller_total": "30000", + "units": "1000000" + } + } + }, + { + "id": "alternatives-require-choice", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + }, + { + "pricing_id": "cpc-usd", + "cost_method": "CPC", + "gross_rate": "2", + "currency": "USD" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [] + }, + "expected": { + "error": "pricing_choice_required" + } + }, + { + "id": "choose-cpc-without-summing-cpm", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + }, + { + "pricing_id": "cpc-usd", + "cost_method": "CPC", + "gross_rate": "2", + "currency": "USD" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [ + { + "id": "production/build", + "delta_flat": "2500" + } + ], + "pricing_id": "cpc-usd" + }, + "expected": { + "result": { + "pricing_id": "cpc-usd", + "currency": "USD", + "agreed_rate": "2", + "flat_charges": "2500", + "media_spend": "47500", + "seller_total": "50000", + "units": "23750" + } + } + }, + { + "id": "choose-cpm-without-summing-cpc", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + }, + { + "pricing_id": "cpc-usd", + "cost_method": "CPC", + "gross_rate": "2", + "currency": "USD" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [], + "pricing_id": "cpm-usd" + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "agreed_rate": "25", + "flat_charges": "0", + "media_spend": "50000", + "seller_total": "50000", + "units": "2000000" + } + } + }, + { + "id": "cpc-rejects-cpm-delta", + "input": { + "pricing": [ + { + "pricing_id": "cpc-usd", + "cost_method": "CPC", + "gross_rate": "2", + "currency": "USD" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [ + { + "id": "audiences/a", + "delta_cpm": "4" + }, + { + "id": "audiences/b", + "delta_cpm": "1.5" + }, + { + "id": "formats/30s", + "delta_cpm": "3" + } + ] + }, + "expected": { + "error": "incompatible_delta_cpm" + } + }, + { + "id": "cpc-does-not-treat-clicks-as-impressions", + "input": { + "pricing": [ + { + "pricing_id": "cpc-usd", + "cost_method": "CPC", + "gross_rate": "2", + "currency": "USD" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [], + "bounds": { + "min_impressions": "1" + } + }, + "expected": { + "error": "unsupported_impression_bound" + } + }, + { + "id": "duplicate-pricing-id", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + }, + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [], + "pricing_id": "cpm-usd" + }, + "expected": { + "error": "duplicate_pricing_id" + } + }, + { + "id": "unknown-pricing-id", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [], + "pricing_id": "absent" + }, + "expected": { + "error": "unknown_pricing_id" + } + }, + { + "id": "currency-mismatch", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "EUR", + "adjustments": [] + }, + "expected": { + "error": "currency_mismatch" + } + }, + { + "id": "select-eur-alternative", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + }, + { + "pricing_id": "cpc-usd", + "cost_method": "CPC", + "gross_rate": "2", + "currency": "USD" + }, + { + "pricing_id": "cpm-eur", + "cost_method": "cpm", + "gross_rate": "20", + "currency": "EUR" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "EUR", + "adjustments": [], + "pricing_id": "cpm-eur" + }, + "expected": { + "result": { + "pricing_id": "cpm-eur", + "currency": "EUR", + "agreed_rate": "20", + "flat_charges": "0", + "media_spend": "50000", + "seller_total": "50000", + "units": "2500000" + } + } + }, + { + "id": "zero-rate", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "0", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [] + }, + "expected": { + "error": "nonpositive_rate" + } + }, + { + "id": "negative-rate", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "-1", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [] + }, + "expected": { + "error": "nonpositive_rate" + } + }, + { + "id": "zero-spend", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "0", + "currency": "USD", + "adjustments": [] + }, + "expected": { + "error": "nonpositive_amount" + } + }, + { + "id": "negative-units", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "units", + "amount": "-1", + "currency": "USD", + "adjustments": [] + }, + "expected": { + "error": "nonpositive_amount" + } + }, + { + "id": "float-not-fixture-decimal", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": 50000.0, + "currency": "USD", + "adjustments": [] + }, + "expected": { + "error": "invalid_number" + } + }, + { + "id": "nonfinite-number", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "NaN", + "currency": "USD", + "adjustments": [] + }, + "expected": { + "error": "invalid_number" + } + }, + { + "id": "exact-decimal-no-binary-rounding", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "0.1", + "currency": "USD", + "floor": "0" + } + ], + "basis": "spend", + "amount": "0.3", + "currency": "USD", + "adjustments": [] + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "agreed_rate": "1/10", + "flat_charges": "0", + "media_spend": "3/10", + "seller_total": "3/10", + "units": "3000" + } + } + }, + { + "id": "fractional-units-not-rounded-up", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "3", + "currency": "USD", + "floor": "0" + } + ], + "basis": "spend", + "amount": "1", + "currency": "USD", + "adjustments": [] + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "agreed_rate": "3", + "flat_charges": "0", + "media_spend": "1", + "seller_total": "1", + "units": "1000/3" + } + } + }, + { + "id": "impression-minimum-after-fees", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [ + { + "id": "production/build", + "delta_flat": "2500" + } + ], + "bounds": { + "min_impressions": "2000000" + } + }, + "expected": { + "error": "outside_min_impressions" + } + }, + { + "id": "impression-maximum", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [], + "bounds": { + "max_impressions": "1999999" + } + }, + "expected": { + "error": "outside_max_impressions" + } + }, + { + "id": "inverted-budget-bounds", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [], + "bounds": { + "min_budget": "60000", + "max_budget": "50000" + } + }, + "expected": { + "error": "inverted_bounds" + } + }, + { + "id": "unknown-extension-is-not-implemented", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "seller.example:engagement", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [] + }, + "expected": { + "error": "unsupported_cost_method" + } + }, + { + "id": "flat-cost-method-is-not-a-flat-fee", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "FLAT", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [] + }, + "expected": { + "error": "unsupported_cost_method" + } + }, + { + "id": "tax-inclusive-needs-separate-rule", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [], + "tax_inclusive": true + }, + "expected": { + "error": "unsupported_field" + } + }, + { + "id": "print-adjustments-need-separate-rule", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [ + { + "id": "agency", + "value_pct": "-15" + } + ] + }, + "expected": { + "error": "unsupported_field" + } + }, + { + "id": "cpmv-uses-thousand-viewable-units", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPMV", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [] + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "agreed_rate": "25", + "flat_charges": "0", + "media_spend": "50000", + "seller_total": "50000", + "units": "2000000" + } + } + }, + { + "id": "cpmv-does-not-imply-all-impressions", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPMV", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [], + "bounds": { + "min_impressions": "1" + } + }, + "expected": { + "error": "unsupported_impression_bound" + } + } + ] +} diff --git a/scripts/check_pricing_fixtures.py b/scripts/check_pricing_fixtures.py new file mode 100644 index 0000000..6c3b097 --- /dev/null +++ b/scripts/check_pricing_fixtures.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +"""Executable discussion profile for #30, #94 and #115, not a wire validator.""" + +import argparse +import json +import re +from fractions import Fraction +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DECIMAL = re.compile(r"-?\d{1,18}(?:\.\d{1,9})?\Z") + + +class ProfileError(ValueError): + pass + + +def require(condition, code): + if not condition: + raise ProfileError(code) + + +def number(value): + # Decimal strings make these test vectors independent of binary floats. + require(isinstance(value, str) and DECIMAL.fullmatch(value), "invalid_number") + return Fraction(value) + + +def shape(obj, required, optional=()): + require(isinstance(obj, dict), "invalid_shape") + require(set(required) <= obj.keys(), "missing_field") + require(obj.keys() <= set(required) | set(optional), "unsupported_field") + + +def resolve(case): + """Resolve already selected adjustments using exact rational arithmetic. + + Input names are fixture adapter keys, not proposed OpenProposal fields. + Only a caller that has established acceptance may supply accepted_counter. + """ + shape(case, ("pricing", "basis", "amount", "currency", "adjustments"), + ("pricing_id", "accepted_counter", "selection_key", "bounds")) + rows = case["pricing"] + require(isinstance(rows, list) and rows, "missing_pricing") + for row in rows: + shape(row, ("pricing_id", "cost_method", "gross_rate", "currency"), ("floor",)) + require(isinstance(row["pricing_id"], str) and row["pricing_id"], "invalid_pricing_id") + ids = [row["pricing_id"] for row in rows] + require(len(ids) == len(set(ids)), "duplicate_pricing_id") + require(len(rows) == 1 or "pricing_id" in case, "pricing_choice_required") + chosen = case.get("pricing_id", ids[0]) + require(chosen in ids, "unknown_pricing_id") + row = rows[ids.index(chosen)] + method = row["cost_method"] + require(isinstance(method, str), "invalid_shape") + method = method.upper() + require(method in ("CPM", "CPMV", "CPC"), "unsupported_cost_method") + require(isinstance(row["currency"], str) and re.fullmatch(r"[A-Z]{3}", row["currency"]), + "invalid_currency") + require(case["currency"] == row["currency"], "currency_mismatch") + require(case["basis"] in ("spend", "units"), "unsupported_basis") + amount = number(case["amount"]) + require(amount > 0, "nonpositive_amount") + rate = number(row["gross_rate"]) + require(rate > 0, "nonpositive_rate") + cpm_delta, flat, factors = Fraction(0), Fraction(0), [] + adjustments = case["adjustments"] + require(isinstance(adjustments, list), "invalid_shape") + adjustment_ids = set() + for adjustment in adjustments: + shape(adjustment, ("id",), ("delta_cpm", "delta_pct", "delta_flat")) + key = adjustment["id"] + require(isinstance(key, str) and key, "invalid_adjustment_id") + require(key not in adjustment_ids, "duplicate_adjustment") + adjustment_ids.add(key) + require(len(adjustment) > 1, "empty_adjustment") + if "delta_cpm" in adjustment: + require(method in ("CPM", "CPMV"), "incompatible_delta_cpm") + cpm_delta += number(adjustment["delta_cpm"]) + if "delta_flat" in adjustment: + fee = number(adjustment["delta_flat"]) + require(fee >= 0, "unsupported_flat_credit") + flat += fee + if "delta_pct" in adjustment: + factor = 1 + number(adjustment["delta_pct"]) + require(factor > 0, "nonpositive_percentage_factor") + factors.append(factor) + rate += cpm_delta + for factor in factors: + rate *= factor + if "accepted_counter" in case: + counter = case["accepted_counter"] + shape(counter, ("rate", "selection_key")) + require(isinstance(case.get("selection_key"), str) and case["selection_key"], + "counter_binding_required") + require(counter["selection_key"] == case["selection_key"], "stale_counter") + # The accepted counter is the final composed rate, not a new base rate. + rate = number(counter["rate"]) + require(rate > 0, "nonpositive_rate") + floor = number(row.get("floor", "0")) + require(floor >= 0, "negative_floor") + require(rate >= floor, "below_floor") + rate_per = 1000 if method in ("CPM", "CPMV") else 1 + if case["basis"] == "spend": + total = amount + require(total >= flat, "fees_exceed_spend") + media = total - flat + units = media / rate * rate_per + else: + units = amount + media = units / rate_per * rate + total = media + flat + bounds = case.get("bounds", {}) + shape(bounds, (), ("min_budget", "max_budget", "min_spend", "min_impressions", "max_impressions")) + values = {key: number(value) for key, value in bounds.items()} + require(all(value >= 0 for value in values.values()), "negative_bound") + for low, high in (("min_budget", "max_budget"), ("min_impressions", "max_impressions")): + require(low not in values or high not in values or values[low] <= values[high], + "inverted_bounds") + require("min_spend" not in values or "max_budget" not in values or + values["min_spend"] <= values["max_budget"], "inverted_bounds") + for name, value in values.items(): + actual = total + if name.endswith("impressions"): + require(method == "CPM", "unsupported_impression_bound") + actual = units + require(actual >= value if name.startswith("min_") else actual <= value, + "outside_" + name) + return {"pricing_id": chosen, "currency": row["currency"], + **{key: str(value) for key, value in { + "agreed_rate": rate, "flat_charges": flat, "media_spend": media, + "seller_total": total, "units": units}.items()}} + + +def evaluate(case): + try: + return {"result": resolve(case)} + except ProfileError as error: + return {"error": str(error)} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--json", action="store_true", help="emit per-vector evidence") + args = parser.parse_args() + corpus = json.loads((ROOT / "fixtures/pricing/cases.json").read_text()) + results = [] + for vector in corpus["cases"]: + actual = evaluate(vector["input"]) + results.append({"id": vector["id"], "passed": actual == vector["expected"], + "actual": actual, "expected": vector["expected"]}) + passed = sum(result["passed"] for result in results) + if args.json: + print(json.dumps({"profile": corpus["profile"], "passed": passed, + "total": len(results), "cases": results}, indent=2)) + else: + for result in results: + print(("PASS " if result["passed"] else "FAIL ") + result["id"]) + if not result["passed"]: + print(json.dumps(result, indent=2)) + print(f"{passed}/{len(results)} pricing discussion vectors pass") + return int(passed != len(results)) + + +if __name__ == "__main__": + raise SystemExit(main()) From 841c9c8eead273f5541722f9ccf09d572db4fa31 Mon Sep 17 00:00:00 2001 From: Aleksander Sekowski Date: Sat, 24 Oct 2026 11:37:05 -0700 Subject: [PATCH 2/3] Keep floor eligibility outside the pricing arithmetic profile --- .../pricing-commitment-arithmetic.md | 197 +++++++----------- fixtures/pricing/cases.json | 125 +++++++++-- scripts/check_pricing_fixtures.py | 5 +- 3 files changed, 183 insertions(+), 144 deletions(-) diff --git a/docs/proposals/pricing-commitment-arithmetic.md b/docs/proposals/pricing-commitment-arithmetic.md index db0aa6a..4e8ac39 100644 --- a/docs/proposals/pricing-commitment-arithmetic.md +++ b/docs/proposals/pricing-commitment-arithmetic.md @@ -1,80 +1,52 @@ -# Resolve one price and account for seller fees once +# Account for seller fees after selecting one price -Implementation feedback for [#30](https://github.com/IABTechLab/OpenProposal/issues/30), -[#94](https://github.com/IABTechLab/OpenProposal/issues/94) and -[#115](https://github.com/IABTechLab/OpenProposal/issues/115), against draft -`44dfaaf618736219426c311ec997b6d37e94b950`. +Discussion wording for [#94](https://github.com/IABTechLab/OpenProposal/issues/94) +and [#115](https://github.com/IABTechLab/OpenProposal/issues/115), against draft +`44dfaaf618736219426c311ec997b6d37e94b950`. The arithmetic also exercises the +final-rate reading discussed in [#30](https://github.com/IABTechLab/OpenProposal/issues/30). +This is a proposed profile, not an adopted requirement or an OpenProposal wire +validator. The normative specification is unchanged. -This is a proposed calculation profile with executable discussion vectors. -It is not an adopted requirement, an OpenProposal schema, or evidence of a -defect in a deployed implementation. Its fixture keys are an adapter format. +## The fee interpretation changes the available media spend -## The proposals currently produce incompatible results +#94 treats `pricing[]` entries as alternatives. #115 adds seller fees as extra +entries and includes them in `commitment.amount`. An extra alternative cannot +simultaneously be an additive fee. Summing all entries instead loses the choice +of cost method, currency and transaction mechanism. -#94 makes `pricing[]` entries alternatives and selects exactly one. -#115 puts seller-billed fees into additional `pricing[]` entries and includes -them in `commitment.amount`. Treating a fee as another alternative does not -add it to the selected price. Summing all rows instead loses #94's choice of -cost method, currency and transaction mechanism. - -There is also an arithmetic consequence. At USD 25 CPM, a USD 50,000 spend -commitment containing a USD 2,500 production fee leaves USD 47,500 for media: +At USD 25 CPM, a USD 50,000 commitment containing a USD 2,500 production fee +leaves USD 47,500 for media: ``` media_spend = 50,000 - 2,500 = 47,500 USD derived_units = 47,500 / 25 * 1,000 = 1,900,000 impressions ``` -Computing units from the whole commitment gives 2,000,000 impressions. Those -units cost USD 50,000 before the fee, for a seller total of USD 52,500. -Therefore #115's statement that `derived_units` is unaffected needs revision -if `amount` includes charges not included in the unit rate. - -## Proposed wording for joint resolution +Two million impressions would cost USD 50,000 before the fee, or USD 52,500 +in total. Therefore #115's statement that `derived_units` is unaffected needs +revision if `amount` includes charges outside the unit rate. -This wording assumes adoption of the alternative-selection part of #94. -It supplies the missing fee basis in #115 and clarifies the counter described -in #30. The working group can decide these pieces together without adopting -the entire cost-method vocabulary. +## Proposed wording for sections 3.1, 5.6 and 5.6.1 -### Sections 3.1 and 5.6: select the price and compose the rate +This wording assumes adoption of the alternative-selection rule in #94. -`pricing[]` entries are alternative prices, not additive charges. A commitment -MUST resolve to exactly one entry under the selection rule in #94. Only that -entry determines the cost method, rate, floor and currency. An implementation -MUST NOT sum alternative entries to compute the buyer's charge. +`pricing[]` entries are alternative prices. A commitment MUST resolve to +exactly one entry under that rule. An implementation MUST NOT sum alternative +entries to compute the buyer's charge. A selected option's `delta_flat` is a one-time seller charge for that line -item commitment. It MUST NOT enter the unit rate or be represented as another -alternative price solely to make it additive. A cost already included in the -unit rate MUST NOT also be added as a flat charge. Each selected option that -is not already included contributes its charge once. - -Unless a final-rate counter has been accepted, the composed rate is the chosen -entry's `gross_rate`, plus applicable `delta_cpm` values, then multiplied by -each applicable `(1 + delta_pct)` factor. For this proposal, `delta_pct` is a -decimal fraction: `0.20` means a 20 percent increase. `delta_cpm` applies only -to CPM and CPMV. Flat charges are outside this calculation. - -An accepted counter stated as the final composed rate MUST become -`agreed_rate` without reapplying the option rate adjustments. It MUST be bound -to the proposal revision, chosen price and resolved selections to which the -seller assented. A changed selection or revision requires a new applicable -acceptance. This rule does not prescribe an acceptance protocol. The floor -comparison uses that same final composed rate. A final-rate counter does not -waive separately identified flat charges unless the accepted terms explicitly -change those charges. - -### Section 5.6.1: define the spend basis +item commitment. It MUST NOT enter the unit rate or become another pricing +alternative solely to make it additive. A cost already included in the unit +rate MUST NOT also be added as a flat charge. Each selected option that is not +already included contributes its charge once. For `basis = spend`, `amount` is the total pre-tax amount payable to the seller for this line item, including applicable seller flat charges. It excludes -amounts payable by the buyer to other parties. When the selected cost method -has a unit rate, the amount available for those units is: +amounts payable by the buyer to other parties. For a selected unit-rate method: ``` -flat_charges = sum of applicable one-time seller charges -media_spend = commitment.amount - flat_charges +flat_charges = sum of applicable one-time seller charges +media_spend = commitment.amount - flat_charges derived_units = media_spend / agreed_rate * rate_per ``` @@ -83,76 +55,63 @@ positive. Flat charges MUST NOT exceed the spend commitment. Zero remaining media spend yields zero informative units and still has to satisfy any applicable minimum-unit requirement. -For `basis = units`, the committed units remain binding and the corresponding -seller total is: +For `basis = units`, the committed units remain binding: ``` -media_spend = commitment.amount / rate_per * agreed_rate -seller_total = media_spend + flat_charges +media_spend = commitment.amount / rate_per * agreed_rate +seller_total = media_spend + flat_charges ``` `min_spend` and budget bounds apply to the seller total on the same pre-tax -basis. Impression bounds apply only when the quantity is actually measured -in impressions; clicks or viewable impressions MUST NOT silently be treated -as the same quantity. The selected entry's currency applies to the rate, -flat charges and budget bounds. No currency conversion is implied. - -The calculations above do not round informative quantities. A displayed -rounded estimate MUST NOT change the committed amount or imply additional -authorized spend. Invoice rounding and minor-unit settlement need a separate -rule if they are to be interoperable. - -### Deliberate limits of this first proposal - -- This profile addresses seller fees already expressible through selected - `delta_flat` options. It does not invent a general fee object. If #115 needs - mandatory charges that cannot use that structure, their representation - needs a separate decision. They cannot be silently omitted. -- The proposed `billing.tax_inclusive` branch of #115 needs a tax amount or a - deterministic tax calculation before its units can be derived. The runner - rejects that branch rather than assuming tax is zero. -- #103's print discounts and surcharges introduce separate calculation bases. - Its `value_pct` and conditions are not interpreted by this profile. -- Cost methods other than CPM, CPMV and CPC are not implemented here. An - unsupported profile case is not a declaration that an OpenProposal is - invalid. Extension definitions and transaction-mechanism eligibility stay - with #94. -- A passing calculation establishes arithmetic consistency only. It does - not establish seller acceptance, inventory reservation or an agreed order. - -## Worked counter example - -A base CPM of USD 30 plus USD 4, USD 1.50 and USD 3 option adjustments gives -USD 38.50. If the seller accepts USD 30 as the final rate for those selections, -the rate is USD 30. Applying the adjustments again incorrectly restores -USD 38.50. A separate USD 2,500 flat fee remains outside that rate: one million -committed impressions cost USD 32,500 in total. - -## Reproduce the vectors +basis. The selected entry's currency applies to rates, flat charges and budget +bounds. No currency conversion is implied. Informative unit estimates MUST NOT +be rounded in a way that changes the committed amount or authorizes extra spend. + +## Arithmetic inputs do not establish acceptance + +The adapter consumes resolved, field-qualified adjustments that are not already +included. Its provisional #30 calculation starts with the chosen `gross_rate`, +adds applicable `delta_cpm`, then multiplies by `(1 + delta_pct)` factors. +`0.20` means a 20 percent increase. Flat charges remain separate. + +If the caller supplies an already accepted final-rate counter, that rate replaces +the composed rate without applying option rate deltas again. The caller must +have established acceptance for the bound proposal revision and selection. +`selection_key` is an opaque fixture binding, not an acceptance protocol. + +The adapter does not evaluate floor eligibility. Every successful result carries +`floor_check: not_checked`. A posted floor is checked only for numeric shape and +nonnegativity. In particular, an explicitly accepted USD 19 rate against a posted +USD 20 floor remains USD 19 in the calculation. The floor does not veto acceptance +already established by the caller. + +How a counter resolves automatically, how selected adjustments compose the floor, +and how seller assent is authenticated remain in #30. That discussion proposes +applying rate adjustments to the base floor. This profile neither adopts that +proposal nor silently compares a final rate with an uncomposed floor. A successful +calculation never authorizes a commitment, reservation or agreed order. + +## Scope and reproduction ``` python3 scripts/check_pricing_fixtures.py python3 scripts/check_pricing_fixtures.py --json ``` -The corpus is [`fixtures/pricing/cases.json`](../../fixtures/pricing/cases.json). -Expected outcomes are fixed independently of the calculator. Cases cover -alternative selection, fee-inclusive spend, unit commitments, counters, -currency, floors, bounds, percentage composition and unsupported semantics. - -Adapter inputs contain only the already resolved selected adjustments that -are not included in the base price. They do not extract selections from a -proposal or implement #22. Adjustment IDs are field-qualified to avoid -counting the same resolved adjustment twice. The caller has already confirmed -mechanism eligibility and seller acceptance of any `accepted_counter`. -`selection_key` is an opaque test binding, not an OpenProposal field or an -implementation of revision authentication. - -Decimal strings and exact rational outputs avoid hidden binary floating point -rounding. For example, `"1000/3"` means the exact informative quantity one -thousand divided by three. This is a fixture convention, not a proposed JSON -wire type. Currency checks cover agreement and shape, not ISO-4217 membership. - -No normative body or field index is changed by this discussion corpus. If -the wording is adopted, update sections 3.1, 5.6 and 5.6.1, the affected field -descriptions in Appendices A and B, and the committed examples together. +[`fixtures/pricing/cases.json`](../../fixtures/pricing/cases.json) contains fixed +expected outcomes independent of the calculator. Decimal strings and rational +outputs avoid binary floating point rounding. `"1000/3"` is an exact informative +quantity, not a proposed JSON wire type. Currency checks cover agreement and +shape, not ISO-4217 membership. The caller supplies a mechanism-eligible choice. + +The profile implements CPM, CPMV and CPC only. It rejects unsupported tax-inclusive +inputs, #103 print adjustments and flat credits. It cannot infer a mandatory fee +that is not represented by the resolved options. General fee objects, tax amounts, +invoice rounding and cost-method extensions need separate decisions. Impression +bounds are checked only for CPM; clicks and viewable impressions are not silently +treated as the same quantity. Unsupported cases are not declarations that an +OpenProposal document is invalid. + +If the fee wording is adopted, update the affected body sections, field +descriptions in Appendices A and B, and committed examples together. The executable +profile is supporting discussion evidence, not a replacement for those changes. diff --git a/fixtures/pricing/cases.json b/fixtures/pricing/cases.json index 0c65d9e..94c6ed8 100644 --- a/fixtures/pricing/cases.json +++ b/fixtures/pricing/cases.json @@ -1,5 +1,5 @@ { - "profile": "pricing-discussion-1", + "profile": "pricing-discussion-2", "cases": [ { "id": "cpm-no-fees", @@ -26,7 +26,8 @@ "flat_charges": "0", "media_spend": "50000", "seller_total": "50000", - "units": "2000000" + "units": "2000000", + "floor_check": "not_checked" } } }, @@ -60,7 +61,8 @@ "flat_charges": "2500", "media_spend": "47500", "seller_total": "50000", - "units": "1900000" + "units": "1900000", + "floor_check": "not_checked" } } }, @@ -94,7 +96,8 @@ "flat_charges": "2500", "media_spend": "50000", "seller_total": "52500", - "units": "2000000" + "units": "2000000", + "floor_check": "not_checked" } } }, @@ -162,7 +165,8 @@ "flat_charges": "2500", "media_spend": "47500", "seller_total": "50000", - "units": "1900000" + "units": "1900000", + "floor_check": "not_checked" } } }, @@ -222,7 +226,8 @@ "flat_charges": "2500", "media_spend": "0", "seller_total": "2500", - "units": "0" + "units": "0", + "floor_check": "not_checked" } } }, @@ -260,7 +265,8 @@ "flat_charges": "3000", "media_spend": "47000", "seller_total": "50000", - "units": "1880000" + "units": "1880000", + "floor_check": "not_checked" } } }, @@ -358,7 +364,8 @@ "flat_charges": "0", "media_spend": "38500", "seller_total": "38500", - "units": "1000000" + "units": "1000000", + "floor_check": "not_checked" } } }, @@ -405,7 +412,8 @@ "flat_charges": "0", "media_spend": "30000", "seller_total": "30000", - "units": "1000000" + "units": "1000000", + "floor_check": "not_checked" } } }, @@ -456,7 +464,8 @@ "flat_charges": "2500", "media_spend": "30000", "seller_total": "32500", - "units": "1000000" + "units": "1000000", + "floor_check": "not_checked" } } }, @@ -525,7 +534,7 @@ } }, { - "id": "counter-below-final-floor", + "id": "accepted-counter-below-posted-floor", "input": { "pricing": [ { @@ -560,11 +569,20 @@ } }, "expected": { - "error": "below_floor" + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "agreed_rate": "19", + "flat_charges": "0", + "media_spend": "30000", + "seller_total": "30000", + "units": "30000000/19", + "floor_check": "not_checked" + } } }, { - "id": "floor-equality-allowed", + "id": "accepted-counter-at-posted-floor", "input": { "pricing": [ { @@ -606,7 +624,8 @@ "flat_charges": "0", "media_spend": "30000", "seller_total": "30000", - "units": "1500000" + "units": "1500000", + "floor_check": "not_checked" } } }, @@ -644,7 +663,8 @@ "flat_charges": "0", "media_spend": "36000", "seller_total": "36000", - "units": "1000000" + "units": "1000000", + "floor_check": "not_checked" } } }, @@ -686,7 +706,8 @@ "flat_charges": "2500", "media_spend": "27000", "seller_total": "29500", - "units": "1000000" + "units": "1000000", + "floor_check": "not_checked" } } }, @@ -763,7 +784,8 @@ "flat_charges": "0", "media_spend": "30000", "seller_total": "30000", - "units": "1000000" + "units": "1000000", + "floor_check": "not_checked" } } }, @@ -831,7 +853,8 @@ "flat_charges": "2500", "media_spend": "47500", "seller_total": "50000", - "units": "23750" + "units": "23750", + "floor_check": "not_checked" } } }, @@ -867,7 +890,8 @@ "flat_charges": "0", "media_spend": "50000", "seller_total": "50000", - "units": "2000000" + "units": "2000000", + "floor_check": "not_checked" } } }, @@ -1037,7 +1061,8 @@ "flat_charges": "0", "media_spend": "50000", "seller_total": "50000", - "units": "2500000" + "units": "2500000", + "floor_check": "not_checked" } } }, @@ -1192,7 +1217,8 @@ "flat_charges": "0", "media_spend": "3/10", "seller_total": "3/10", - "units": "3000" + "units": "3000", + "floor_check": "not_checked" } } }, @@ -1221,7 +1247,8 @@ "flat_charges": "0", "media_spend": "1", "seller_total": "1", - "units": "1000/3" + "units": "1000/3", + "floor_check": "not_checked" } } }, @@ -1418,7 +1445,8 @@ "flat_charges": "0", "media_spend": "50000", "seller_total": "50000", - "units": "2000000" + "units": "2000000", + "floor_check": "not_checked" } } }, @@ -1445,6 +1473,57 @@ "expected": { "error": "unsupported_impression_bound" } + }, + { + "id": "posted-floor-does-not-establish-eligibility", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "30" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [] + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "agreed_rate": "25", + "flat_charges": "0", + "media_spend": "50000", + "seller_total": "50000", + "units": "2000000", + "floor_check": "not_checked" + } + } + }, + { + "id": "negative-posted-floor-is-malformed-metadata", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "-1" + } + ], + "basis": "spend", + "amount": "50000", + "currency": "USD", + "adjustments": [] + }, + "expected": { + "error": "negative_floor" + } } ] } diff --git a/scripts/check_pricing_fixtures.py b/scripts/check_pricing_fixtures.py index 6c3b097..44f3fc2 100644 --- a/scripts/check_pricing_fixtures.py +++ b/scripts/check_pricing_fixtures.py @@ -99,7 +99,8 @@ def resolve(case): require(rate > 0, "nonpositive_rate") floor = number(row.get("floor", "0")) require(floor >= 0, "negative_floor") - require(rate >= floor, "below_floor") + # A posted automatic-resolution floor cannot veto explicit acceptance. + # This arithmetic adapter does not choose or evaluate floor semantics. rate_per = 1000 if method in ("CPM", "CPMV") else 1 if case["basis"] == "spend": total = amount @@ -126,7 +127,7 @@ def resolve(case): actual = units require(actual >= value if name.startswith("min_") else actual <= value, "outside_" + name) - return {"pricing_id": chosen, "currency": row["currency"], + return {"pricing_id": chosen, "currency": row["currency"], "floor_check": "not_checked", **{key: str(value) for key, value in { "agreed_rate": rate, "flat_charges": flat, "media_spend": media, "seller_total": total, "units": units}.items()}} From e51ee17d2c74d166dd75c3da29698de4e8a998fa Mon Sep 17 00:00:00 2001 From: Aleksander Sekowski Date: Sun, 25 Oct 2026 11:01:59 -0700 Subject: [PATCH 3/3] Clarify fee basis and allow zero-rate unit commitments --- .../pricing-commitment-arithmetic.md | 29 +- fixtures/pricing/cases.json | 374 +++++++++++++++++- scripts/check_pricing_fixtures.py | 4 +- 3 files changed, 391 insertions(+), 16 deletions(-) diff --git a/docs/proposals/pricing-commitment-arithmetic.md b/docs/proposals/pricing-commitment-arithmetic.md index 4e8ac39..772f212 100644 --- a/docs/proposals/pricing-commitment-arithmetic.md +++ b/docs/proposals/pricing-commitment-arithmetic.md @@ -36,13 +36,14 @@ entries to compute the buyer's charge. A selected option's `delta_flat` is a one-time seller charge for that line item commitment. It MUST NOT enter the unit rate or become another pricing -alternative solely to make it additive. A cost already included in the unit -rate MUST NOT also be added as a flat charge. Each selected option that is not +alternative. A seller fee MUST NOT be a `pricing[]` entry. A cost already +included in the unit rate MUST NOT also be added as a flat charge. Each selected option that is not already included contributes its charge once. -For `basis = spend`, `amount` is the total pre-tax amount payable to the seller -for this line item, including applicable seller flat charges. It excludes -amounts payable by the buyer to other parties. For a selected unit-rate method: +For `basis = spend`, `amount` is the seller total, on the same terms as +`agreed_rate`, including selected `delta_flat` charges (section 3.1). This does +not choose gross or net commission treatment, or decide the tax question in +#115. For a selected unit-rate method: ``` flat_charges = sum of applicable one-time seller charges @@ -50,21 +51,22 @@ media_spend = commitment.amount - flat_charges derived_units = media_spend / agreed_rate * rate_per ``` -`rate_per` is 1,000 for CPM and CPMV, and 1 for CPC. The agreed rate MUST be -positive. Flat charges MUST NOT exceed the spend commitment. Zero remaining -media spend yields zero informative units and still has to satisfy any +`rate_per` is 1,000 for CPM and CPMV, and 1 for CPC. For `basis = spend`, the +agreed rate MUST be positive. Flat charges MUST NOT exceed the spend commitment. +Zero remaining media spend yields zero informative units and still has to satisfy any applicable minimum-unit requirement. -For `basis = units`, the committed units remain binding: +For `basis = units`, the committed units remain binding. The agreed rate MAY +be zero for added-value units, and MUST NOT be negative: ``` media_spend = commitment.amount / rate_per * agreed_rate seller_total = media_spend + flat_charges ``` -`min_spend` and budget bounds apply to the seller total on the same pre-tax -basis. The selected entry's currency applies to rates, flat charges and budget -bounds. No currency conversion is implied. Informative unit estimates MUST NOT +`min_spend` and budget bounds apply to the seller total on the same basis as +`agreed_rate`. The selected entry's currency applies to rates, flat charges and +budget bounds. No currency conversion is implied. Informative unit estimates MUST NOT be rounded in a way that changes the committed amount or authorizes extra spend. ## Arithmetic inputs do not establish acceptance @@ -104,7 +106,8 @@ outputs avoid binary floating point rounding. `"1000/3"` is an exact informative quantity, not a proposed JSON wire type. Currency checks cover agreement and shape, not ISO-4217 membership. The caller supplies a mechanism-eligible choice. -The profile implements CPM, CPMV and CPC only. It rejects unsupported tax-inclusive +The profile implements CPM, CPMV and CPC on a tax-exclusive basis only. That +fixture limit does not decide the normative tax basis. It rejects tax-inclusive inputs, #103 print adjustments and flat credits. It cannot infer a mandatory fee that is not represented by the resolved options. General fee objects, tax amounts, invoice rounding and cost-method extensions need separate decisions. Impression diff --git a/fixtures/pricing/cases.json b/fixtures/pricing/cases.json index 94c6ed8..958ad4b 100644 --- a/fixtures/pricing/cases.json +++ b/fixtures/pricing/cases.json @@ -1,5 +1,5 @@ { - "profile": "pricing-discussion-2", + "profile": "pricing-discussion-3", "cases": [ { "id": "cpm-no-fees", @@ -1524,6 +1524,378 @@ "expected": { "error": "negative_floor" } + }, + { + "id": "units-fee-inclusive-min_spend-11000", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "units", + "amount": "400000", + "currency": "USD", + "adjustments": [ + { + "id": "production/build", + "delta_flat": "1500" + } + ], + "bounds": { + "min_spend": "11000" + } + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "floor_check": "not_checked", + "agreed_rate": "25", + "flat_charges": "1500", + "media_spend": "10000", + "seller_total": "11500", + "units": "400000" + } + } + }, + { + "id": "units-fee-inclusive-min_spend-11500", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "units", + "amount": "400000", + "currency": "USD", + "adjustments": [ + { + "id": "production/build", + "delta_flat": "1500" + } + ], + "bounds": { + "min_spend": "11500" + } + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "floor_check": "not_checked", + "agreed_rate": "25", + "flat_charges": "1500", + "media_spend": "10000", + "seller_total": "11500", + "units": "400000" + } + } + }, + { + "id": "units-fee-inclusive-min_spend-12000", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "units", + "amount": "400000", + "currency": "USD", + "adjustments": [ + { + "id": "production/build", + "delta_flat": "1500" + } + ], + "bounds": { + "min_spend": "12000" + } + }, + "expected": { + "error": "outside_min_spend" + } + }, + { + "id": "units-fee-inclusive-min_budget-11000", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "units", + "amount": "400000", + "currency": "USD", + "adjustments": [ + { + "id": "production/build", + "delta_flat": "1500" + } + ], + "bounds": { + "min_budget": "11000" + } + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "floor_check": "not_checked", + "agreed_rate": "25", + "flat_charges": "1500", + "media_spend": "10000", + "seller_total": "11500", + "units": "400000" + } + } + }, + { + "id": "units-fee-inclusive-min_budget-11500", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "units", + "amount": "400000", + "currency": "USD", + "adjustments": [ + { + "id": "production/build", + "delta_flat": "1500" + } + ], + "bounds": { + "min_budget": "11500" + } + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "floor_check": "not_checked", + "agreed_rate": "25", + "flat_charges": "1500", + "media_spend": "10000", + "seller_total": "11500", + "units": "400000" + } + } + }, + { + "id": "units-fee-inclusive-min_budget-12000", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "units", + "amount": "400000", + "currency": "USD", + "adjustments": [ + { + "id": "production/build", + "delta_flat": "1500" + } + ], + "bounds": { + "min_budget": "12000" + } + }, + "expected": { + "error": "outside_min_budget" + } + }, + { + "id": "zero-rate-added-value-units-fee-0", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "0", + "currency": "USD", + "floor": "20" + } + ], + "basis": "units", + "amount": "400000", + "currency": "USD", + "adjustments": [] + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "floor_check": "not_checked", + "agreed_rate": "0", + "flat_charges": "0", + "media_spend": "0", + "seller_total": "0", + "units": "400000" + } + } + }, + { + "id": "zero-rate-added-value-units-fee-1500", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "0", + "currency": "USD", + "floor": "20" + } + ], + "basis": "units", + "amount": "400000", + "currency": "USD", + "adjustments": [ + { + "id": "production/build", + "delta_flat": "1500" + } + ] + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "floor_check": "not_checked", + "agreed_rate": "0", + "flat_charges": "1500", + "media_spend": "0", + "seller_total": "1500", + "units": "400000" + } + } + }, + { + "id": "units-accepted-counter-zero", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "units", + "amount": "400000", + "currency": "USD", + "adjustments": [ + { + "id": "production/build", + "delta_flat": "1500" + } + ], + "selection_key": "accepted-free-units", + "accepted_counter": { + "rate": "0", + "selection_key": "accepted-free-units" + } + }, + "expected": { + "result": { + "pricing_id": "cpm-usd", + "currency": "USD", + "floor_check": "not_checked", + "agreed_rate": "0", + "flat_charges": "1500", + "media_spend": "0", + "seller_total": "1500", + "units": "400000" + } + } + }, + { + "id": "units-accepted-counter-negative", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "25", + "currency": "USD", + "floor": "20" + } + ], + "basis": "units", + "amount": "400000", + "currency": "USD", + "adjustments": [ + { + "id": "production/build", + "delta_flat": "1500" + } + ], + "selection_key": "accepted-free-units", + "accepted_counter": { + "rate": "-1", + "selection_key": "accepted-free-units" + } + }, + "expected": { + "error": "nonpositive_rate" + } + }, + { + "id": "negative-rate-units", + "input": { + "pricing": [ + { + "pricing_id": "cpm-usd", + "cost_method": "CPM", + "gross_rate": "-1", + "currency": "USD", + "floor": "20" + } + ], + "basis": "units", + "amount": "400000", + "currency": "USD", + "adjustments": [ + { + "id": "production/build", + "delta_flat": "1500" + } + ] + }, + "expected": { + "error": "nonpositive_rate" + } } ] } diff --git a/scripts/check_pricing_fixtures.py b/scripts/check_pricing_fixtures.py index 44f3fc2..a943f76 100644 --- a/scripts/check_pricing_fixtures.py +++ b/scripts/check_pricing_fixtures.py @@ -62,7 +62,7 @@ def resolve(case): amount = number(case["amount"]) require(amount > 0, "nonpositive_amount") rate = number(row["gross_rate"]) - require(rate > 0, "nonpositive_rate") + require(rate >= 0, "nonpositive_rate") cpm_delta, flat, factors = Fraction(0), Fraction(0), [] adjustments = case["adjustments"] require(isinstance(adjustments, list), "invalid_shape") @@ -96,7 +96,7 @@ def resolve(case): require(counter["selection_key"] == case["selection_key"], "stale_counter") # The accepted counter is the final composed rate, not a new base rate. rate = number(counter["rate"]) - require(rate > 0, "nonpositive_rate") + require(rate > 0 if case["basis"] == "spend" else rate >= 0, "nonpositive_rate") floor = number(row.get("floor", "0")) require(floor >= 0, "negative_floor") # A posted automatic-resolution floor cannot veto explicit acceptance.