packages feed

tax-ato-2024.1: src/Data/Tax/ATO/PrivateHealthInsuranceRebate.hs

-- This file is part of hs-tax-ato
-- Copyright (C) 2021  Fraser Tweedale
--
-- hs-tax-ato is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU Affero General Public License for more details.
--
-- You should have received a copy of the GNU Affero General Public License
-- along with this program.  If not, see <http://www.gnu.org/licenses/>.

{-# LANGUAGE RankNTypes #-}

{- |

Types and functions for the Private Health Insurance Rebate.

-}
module Data.Tax.ATO.PrivateHealthInsuranceRebate
  (
    PrivateHealthInsuranceRebateRates
  , PrivateHealthInsurancePolicyDetail(..)
  , BenefitCode(..)
  , assessExcessPrivateHealthRebate
  , HealthInsurerID
  , MembershipNumber
  ) where

import Data.List (find)

import Control.Lens
import Data.Tax
import Data.Tax.ATO.Rounding

type HealthInsurerID = String
type MembershipNumber = String

data BenefitCode
  = BenefitCode30 -- ^ Under 65, 1 July to 31 March
  | BenefitCode31 -- ^ Under 65, 1 April to 30 June
  | BenefitCode35 -- ^ 65 to 69, 1 July to 31 March
  | BenefitCode36 -- ^ 65 to 69, 1 April to 30 June
  | BenefitCode40 -- ^ 70 or over, 1 July to 31 March
  | BenefitCode41 -- ^ 70 or over, 1 April to 30 June

-- | Include these data in your tax return via the
-- 'Data.Tax.ATO.privateHealthInsurancePolicyDetails' field.
data PrivateHealthInsurancePolicyDetail a =
  PrivateHealthInsurancePolicyDetail
    HealthInsurerID
    MembershipNumber
    (Money a) -- ^ premiums eligible for rebate
    (Money a) -- ^ rebate received
    BenefitCode

-- | A line of rebate rates.
--
-- The first field is the upper income threshold for single persons
-- (inclusive) for the given rate.  Thresholds must be given in
-- increasing order.
--
-- The second, third and fourth fields are the rebate rates for when
-- the oldest person on the policy is aged, respectively: under 65;
-- 65 to 69; 70 or older.  Each of these values is a pair.
--
-- The first subfield is the rebate for 1 July to 31 March.
--
-- The second subfield is the rebate for 1 April to 30 June.
--
-- An income that exceeds the highest threshold implicitly gets a
-- rebate of 0%.
--
type PrivateHealthInsuranceRebateRatesLine a
  = (a, (a, a), (a, a), (a, a))
type PrivateHealthInsuranceRebateRates a
  = [PrivateHealthInsuranceRebateRatesLine a]

byBenefitCode
  :: BenefitCode
  -> Lens' (PrivateHealthInsuranceRebateRatesLine a) a
byBenefitCode code = case code of
  BenefitCode30 -> _2 . _1
  BenefitCode31 -> _2 . _2
  BenefitCode35 -> _3 . _1
  BenefitCode36 -> _3 . _2
  BenefitCode40 -> _4 . _1
  BenefitCode41 -> _4 . _2

getRebateRate
  :: (Ord a, Num a)
  => Money a
  -> BenefitCode
  -> PrivateHealthInsuranceRebateRates a
  -> Tax (Money a) (Money a)
getRebateRate income code rates =
  case find (\line -> income <= Money (view _1 line)) rates of
    Nothing -> mempty
    Just rec -> flat $ view (byBenefitCode code) rec

-- | Compute rebates received minus rebate entitlements.
-- Therefore a positive result is tax DUE, and a
-- negative result is a tax CREDIT.
--
assessExcessPrivateHealthRebate
  :: (RealFrac a)
  => Money a          -- ^ income for MLS purposes
  -> Maybe (Money a)  -- ^ spouse income for MLS purposes
  -> Integer          -- ^ number of dependents
  -> PrivateHealthInsuranceRebateRates a
  -> [PrivateHealthInsurancePolicyDetail a]
  -> Money a
assessExcessPrivateHealthRebate income spouseIncome dependents rates =
  foldMap f
  where

  -- The family income threshold is double the single threshold,
  -- increased by $1,500 for each Medicare levy surcharge dependent
  -- child after the first child.
  factor = maybe (if dependents > 0 then 2 else 1) (const 2) spouseIncome
  increase = 1500 * max 0 (fromIntegral dependents - 1)
  preppedRates = over (traverse . _1) ((+ increase) . (* factor)) rates

  preppedIncome = maybe income (<> income) spouseIncome

  f (PrivateHealthInsurancePolicyDetail _ _ eligible received code) =
    let rate = getRebateRate preppedIncome code preppedRates
    in received $-$ getTax (fmap roundCents rate) eligible