defmodule TradeMachine.GMGradeV2.Pillars.CapFlexibility.TaxApronExposure do @moduledoc """ Scores AVC's Tax & Apron Exposure metric from a validated `CapState`. `CapState` owns the tax bill, repeater provenance, financial zone, and signed threshold distances. This metric only normalizes the projected bill by the salary cap and applies the curve for the already-classified zone. """ alias TradeMachine.GMGradeV2.ModelVersion alias TradeMachine.GMGradeV2.Result.MetricResult @doc "Returns the full-precision Tax & Apron Exposure metric result." @spec score(map()) :: MetricResult.t() def score(%{ season: season, financial_zone: zone, taxpayer: taxpayer?, thresholds: thresholds, cushions: cushions, luxury_tax: luxury_tax }) do salary_cap = thresholds |> Map.fetch!(season) |> Map.fetch!(:salary_cap) tax_bill_ratio = luxury_tax.bill / salary_cap %MetricResult{ key: :tax_apron_exposure, version: ModelVersion.model_version(), score: exposure_score(zone, tax_bill_ratio), inputs: %{ financial_zone: zone, salary_cap: salary_cap, projected_tax_bill: luxury_tax.bill, tax_bill_ratio: tax_bill_ratio }, supporting: %{ taxpayer: taxpayer?, repeater: luxury_tax.repeater, tax_cushion: cushions.tax_cushion, first_apron_cushion: cushions.first_apron_cushion, second_apron_cushion: cushions.second_apron_cushion }, explanation: "Tax & Apron Exposure scores the #{label(zone)} zone using the projected " <> "engine tax bill and recorded repeater assumption." } end @doc "Applies AVC's zone-specific tax-bill penalty." @spec exposure_score(atom(), number()) :: float() def exposure_score(zone, _tax_bill_ratio) when zone in [:below_cap, :over_cap_below_tax], do: 100.0 def exposure_score(:taxpayer_below_first_apron, tax_bill_ratio) do 80.0 - min(20.0, tax_bill_ratio * 50.0) end def exposure_score(:between_aprons, tax_bill_ratio) do 45.0 - min(15.0, tax_bill_ratio * 37.5) end def exposure_score(:above_second_apron, tax_bill_ratio) do 15.0 - min(15.0, tax_bill_ratio * 37.5) end defp label(:below_cap), do: "below-cap" defp label(:over_cap_below_tax), do: "over-cap, below-tax" defp label(:taxpayer_below_first_apron), do: "taxpayer, below-first-apron" defp label(:between_aprons), do: "between-aprons" defp label(:above_second_apron), do: "above-second-apron" end