Skip to content

FOCUS Prices: ""negotiated"" discount on SavingsPlan rows mirrors the on-demand meter - original SP MarketPrice/UnitPrice are dropped #2198

Open

Description

Area: FinOps hubs / FOCUS price sheet transform (Prices_transform_v1_2(), Prices_v1_2())

Summary

On SavingsPlan rows, ListUnitPrice, ContractedUnitPrice, and x_BaseUnitPrice are all back-filled from the matching on-demand (Consumption) row, and the row's raw MarketPrice / UnitPrice are discarded (raw UnitPrice survives only as x_EffectiveUnitPrice). As a result the negotiated-discount columns on SP rows are not SP-specific — they just echo the underlying PayGo meter's discount, so you cannot compute a "negotiated discount per SP meter" from Prices_v1_2().

Where (Prices_transform_v1_2())

| extend ContractedUnitPrice = iff(x_SkuPriceType != 'SavingsPlan', UnitPrice, real(null))
| extend ListUnitPrice       = iff(x_SkuPriceType != 'SavingsPlan', MarketPrice, real(null))
...
// SP rows look up the Consumption twin by (MeterId, ProductId, SkuId, Tier, OfferId) - NOTE: no Term
| lookup kind=leftouter (Consumption | distinct tmp_SavingsPlanKey, ListUnitPrice, ContractedUnitPrice, x_BaseUnitPrice) on tmp_SavingsPlanKey
| extend ListUnitPrice       = coalesce(ListUnitPrice, ListUnitPrice1)
| extend ContractedUnitPrice = coalesce(ContractedUnitPrice, ContractedUnitPrice1)
| extend x_EffectiveUnitPrice = iff(x_SkuPriceType == 'SavingsPlan', UnitPrice, real(null))
| extend x_ContractedUnitPriceDiscount = ListUnitPrice - ContractedUnitPrice

Consequences

  1. x_ContractedUnitPriceDiscount(Percent) on an SP row = the on-demand meter's negotiated discount, not anything about the SP. Across a tenant it collapses to the flat PayGo EA/MCA discount (e.g. a uniform ~24.5% on compute), which is misleading when read as "SP negotiated discount."
  2. Because the back-fill key omits Term, ListUnitPrice / ContractedUnitPrice are identical for the 1-yr and 3-yr SP rows of the same meter; only x_EffectiveUnitPrice varies by term. So x_ContractedUnitPriceDiscountPercent is term-independent on SP rows.
  3. The original SP MarketPrice is not retained in any output column of Prices_v1_2().

Questions / requests

  • Is populating x_ContractedUnitPriceDiscount* on SP rows intended, given it only reflects the on-demand twin?
  • Could the transform preserve the raw SP MarketPrice (e.g. as x_SkuMarketPrice / x_SavingsPlanListPrice) so consumers can distinguish SP-native list pricing from the back-filled on-demand list price?
  • Suggest documenting that, for SP rows, negotiated-discount columns are inherited from the on-demand meter and only x_EffectiveUnitPriceDiscount* reflects the SP's own benefit.

Observed with toolkit v14, FOCUS 1.2.

Activity

  1. microsoft-github-policy-service commented on Jun 26, 2026

    @microsoft-github-policy-service

    Thank you for submitting this issue! We try to respond within 3 business days. In the meantime:

  2. RolandKrummenacher commented on Jun 28, 2026

    @RolandKrummenacher
    Collaborator

    Hi Dirk Brinkmann (@DirkBrinkmann) — this is an excellent, precise write-up, and your reading of the transform is correct. Thanks for taking the time.

    To confirm in plain terms: for SavingsPlan rows, Prices_transform_v1_2() currently drops the row's own MarketPrice/UnitPrice (raw UnitPrice survives only as x_EffectiveUnitPrice) and back-fills ListUnitPrice/ContractedUnitPrice/x_BaseUnitPrice from the matching on-demand (Consumption) meter. So, exactly as you found:

    • the negotiated-discount columns on SP rows reflect the on-demand twin's discount, not anything SP-specific;
    • because the back-fill key doesn't include term, the 1-year and 3-year SP rows of the same meter get identical List/Contracted prices (only x_EffectiveUnitPrice varies by term); and
    • the raw SP MarketPrice isn't retained in any output column.

    This relates to known behavior in the price-sheet transform — the cross-row SP→Consumption lookup is tracked in #1625, and an earlier, narrower report of the SP MarketPrice being nulled is in #2115. Your write-up adds useful detail on the SavingsPlan-specific consequences.

    In the meantime, if you need the SP-native list price you can read it straight from the raw data — the MarketPrice/UnitPrice on the SavingsPlan rows in Prices_raw carry the SP's own retail/contracted rates before the transform overwrites them.

    Thanks again — really useful analysis.

  3. RolandKrummenacher commented on Jun 28, 2026

    @RolandKrummenacher
    Collaborator

    🔍 Root-cause analysis (engineering)

    Root cause: Confirmed in current dev. In Prices_transform_v1_2() (src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts/IngestionSetup_v1_2.kql):

    • :55-56 null out ContractedUnitPrice/ListUnitPrice for x_SkuPriceType == 'SavingsPlan' (iff(x_SkuPriceType != 'SavingsPlan', UnitPrice/MarketPrice, real(null))), discarding the SP row's own UnitPrice/MarketPrice. Raw UnitPrice is retained only as x_EffectiveUnitPrice.
    • :66 builds tmp_SavingsPlanKey = strcat(x_SkuMeterId, x_SkuProductId, x_SkuId, x_SkuTier, x_SkuOfferId) — no x_SkuTerm.
    • :80-86 lookup kind=leftouter against Consumption rows (distinct tmp_SavingsPlanKey, ListUnitPrice, ContractedUnitPrice, x_BaseUnitPrice) and coalesce the nulled SP columns from the on-demand twin.

    Net: SP ListUnitPrice/ContractedUnitPrice/x_BaseUnitPrice are the on-demand meter's values, so x_ContractedUnitPriceDiscount(Percent) = ListUnitPrice - ContractedUnitPrice on an SP row is the PayGo meter's negotiated discount (collapses to the flat EA/MCA compute discount tenant-wide) and is term-independent because the key omits term. The raw SP MarketPrice appears in no output column. All three of the reporter's claims reproduce exactly.

    Scope / impact: All v1_2 deployments (ADX + Fabric), EA and MCA, any tenant with SavingsPlan coverage. Affects anyone deriving an "SP negotiated discount" from Prices_v1_2() and any join of SP rows to costs on price keys. Same root code underlies v1_0 (IngestionSetup_v1_0.kql:64-65,75,90).

    Relationship: This is the SavingsPlan-specific face of #1625 (cross-row Prices_transform operations broken by per-batch ADX update-policy execution). PR #2037 (closed unmerged 2026-03-25, mergedAt: null) already designed the fix: remove the SP price-nulling and the Consumption back-fill lookup entirely, keeping the SP row's own MarketPrice→ListUnitPrice and UnitPrice→ContractedUnitPrice, with EA/MCA price-sheet validation cited in the PR confirming those raw fields are the SP retail/contracted rates. That approach directly resolves the reporter's points 1 and 3. The narrower #2115 (MarketPrice nulled for SP) is a subset. #2178 (SP lookup fan-out) was self-retracted by its author as unverified.

    Fix options:

    1. Revive PR Fix: Prices transform cross-row operations broken by per-batch execution #2037's approach — drop the SP nulling (:55-56) and the Consumption lookup (:80-86); SP rows keep their own list/contracted prices. Fixes all three points + the per-batch correctness problem in Prices_transform_v1_0 function does not work as intended #1625. Trade-off: SP price-column semantics change (consumers relying on the on-demand-twin values would see different numbers); re-confirm the EA/MCA validation before merge.
    2. Keep the back-fill but additionally preserve raw SP pricing in new x_ columns (e.g. x_SkuMarketPrice) and document that SP discount columns are inherited. Trade-off: additive/backward-compatible, but leaves the misleading discount semantics and doesn't fix Prices_transform_v1_0 function does not work as intended #1625's per-batch breakage.
    3. Document-only — note that on SP rows the negotiated-discount columns are inherited from the on-demand meter and only x_EffectiveUnitPrice(Discount*) reflects the SP's own benefit. Trade-off: zero code change; data stays misleading.

    Recommendation: Needs a team decision before committing to a direction — this isn't a clear-cut fix. Option 1 (revive #2037) is the most complete and was already drafted with EA/MCA validation, but PR #2037 was closed unmerged, so changing the SP price-column semantics is evidently a non-trivial call (backward-compat for any consumer currently reading the on-demand-twin values, and the per-batch execution constraint behind #1625). Options 1 (semantic fix), 2 (additive raw-price x_ columns, back-compat), and 3 (document-only) should be weighed together, ideally folded into the #1625 work item rather than decided in triage.

  4. flanakin commented on Aug 26, 2026

    @flanakin
    Collaborator

    🤖 [AI] Update: revisited reviving PR #2037's approach (remove the SP→Consumption price backfill lookup) for this release, but per repo owner feedback: #2037 traded one incomplete state for another — it fixed some rows while breaking others, without solving the actual root cause (ADX update policies executing per-ingestion-batch, so cross-row operations like the SP↔Consumption lookup only ever see the current file's data, not the full price sheet).

    The real fix likely needs a two-phase ingestion approach: ingest raw price rows per-batch as today, then run a second pass after all files for a given export have landed to resolve cross-row logic (SP price backfill, eligibility, etc.) against the complete dataset — conceptually similar to how #2246 sidestepped this same per-batch limitation for commitment discount eligibility by moving to a separately-computed reference table instead of an in-transform lookup.

    This is real design work, not a quick fix, so moving to v16 rather than rushing something into this release. Related: #1625 (the general per-batch cross-row problem), #2037 (closed, incomplete fix), #2115 (narrower MarketPrice-nulled report).

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

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions