diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,112 @@
+## Version 2025.1
+
+### Features and enhancements
+
+- Overhauled payment summaries.  More comprehensive data types and
+  optics are provided in module `Data.Tax.ATO.PaymentSummary`.
+
+  The old `PaymentSummary` constructor persists as a (deprecated)
+  pattern synonym, for backwards compatibility.  It will be removed
+  in a future release.
+
+- Implemented **reportable fringe benefits**. .  Use the
+  `fringeBenefitsEmployerNotExempt` or
+  `fringeBenefitsEmployerExempt` constructors, along with the
+  `reportableFringeBenefits` lens to add it to the payment summary.
+  The amount is properly included in the surcharge and student loan
+  repayment income calculations.
+
+- Implemented **allowances**.  A list of allowances can be added to
+  a payment summary via the `allowances` lens.  Construct allowance
+  values with `allowance :: String -> Money a -> Allowance a`.
+
+- Implemented **non-super lump sum A, B, D and E** payments.
+
+- Implemented additional payment summary types:
+  - Foreign Employment
+  - Business and personal services income
+  - Withholding where ABN not quoted
+
+- New module `Data.Tax.ATO.Depreciation` provides types and
+  functions for calculating decline in value (for deductions).
+  Use the `DepreciatingAsset` data type to define depreciating
+  assets, and the `declineInValue` function for calculating the
+  decline in value in a given financial year.
+
+- Added the "cents-per-kilometre" method for car expense deductions.
+  Usage: `applyCentsPerKilometreMethod @FY kms`.  ([#13])
+
+- Added home office expense deduction methods ([#7]).  Usage
+
+  ```
+  applyFixedRateMethod @FY hours
+  applyShortcutMethod @FY hours
+  applyFixedRateMethodPre2023 @FY hours
+  ```
+
+- Implement Medicare levy low income family thresholds, including
+  increase based on number of dependents.  Senior and pensioners
+  individual and family thresholds remain unimplemented.
+
+- CGT assessment types were overhauled and now contain more
+  information about total capital gain and applied / unapplied
+  losses.  This is step toward a more complete CGT implementation.
+  Remaining gaps include foreign withholding credits and separate
+  treatment of collectables.
+
+- Multiple ESS statements can now be added to a tax return.  Also,
+  the `essEmployeeDetails` field has been added, and
+  `newESSStatement` was updated to take a `PayerDetails`.  This is a
+  breaking change.  ([#16])
+
+- Added `division293Income` which calculates the income component of
+  Division 293 tax calculations.  Some elements are not included
+  because the relevant feature is not yet implemented (e.g. rental
+  losses or FHSS releases).  ([#12])
+
+- Added `paygInstalmentIncome`, which calculates the PAYG instalment
+  income.  Some elements are not included because the relevant
+  features are not yet implement (e.g. rent, partnership and trust
+  income, PSI).
+
+- Added the `Data.Tax.ATO.ABN` and `Data.Tax.ATO.TFN` modules, which
+  have more representative data types, conversion functions, and
+  proper validation.  `IsString` instances are provided for
+  compatibility with existing code.
+
+- `TaxReturnInfo` pretty printing now includes more detailed ESS and
+  CGT data, to help with preparation of tax returns.
+
+- `TaxAssessment` pretty printing now separately reports Medicare
+  levy, Medicare levy surcharge, and the various non-refundable and
+  refundable tax offsets.  The formatting closely resembles the ATO
+  *notice of assessment* layout.
+
+- Add `FY2025` module.  All rates and thresholds are up to date.
+
+- Added some new helper functions to the `Data.Tax.ATO.FY` module:
+  - `fromProxy :: (FinancialYear y) => Proxy y -> Year`
+  - `daysInYear :: Year -> Integer`
+  - `daysInYearFromProxy :: FinancialYear y => Proxy y -> Integer`
+
+### Bug fixes
+
+- Non-refundable offsets now cannot reduce liability below $0, and
+  the order of application of the various liabilities, offsets and
+  credits has been fixed.
+
+- Employee share scheme TFN amounts withheld are now included in
+  assessment calculations ([#8]).
+
+- Fix FY2024 private health insurance rebate tier thresholds.
+
+[#7]: https://github.com/frasertweedale/hs-tax-ato/issues/7
+[#8]: https://github.com/frasertweedale/hs-tax-ato/issues/8
+[#12]: https://github.com/frasertweedale/hs-tax-ato/issues/12
+[#13]: https://github.com/frasertweedale/hs-tax-ato/issues/13
+[#16]: https://github.com/frasertweedale/hs-tax-ato/issues/16
+
+
 ## Version 2024.1
 
 - The minimum supported version of GHC is now 9.2, due to a
diff --git a/src/Data/Tax/ATO.hs b/src/Data/Tax/ATO.hs
--- a/src/Data/Tax/ATO.hs
+++ b/src/Data/Tax/ATO.hs
@@ -1,7 +1,5 @@
-{-# LANGUAGE PolyKinds #-}
-
 -- This file is part of hs-tax-ato
--- Copyright (C) 2018-2021  Fraser Tweedale
+-- Copyright (C) 2018-2025  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
@@ -25,14 +23,11 @@
 Lots of things are not implemented, including (but not limited to):
 __ETPs__, income from __partnerships and trusts__,
 __superannuation__ income streams and lump payments, tax losses from
-previous years, __Medicare levy reduction/exemption__, adjustments,
+previous years, Medicare levy exemptions, adjustments,
 and variations based on family income and dependents.
 
 -}
 
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE MultiParamTypeClasses #-}
-
 module Data.Tax.ATO
   (
   -- * Synopsis
@@ -42,14 +37,15 @@
     TaxReturnInfo
   , newTaxReturnInfo
   , newTaxReturnInfoForTables
-  , taxableIncome
 
   -- ** Income
 
   -- *** PAYG Payment Summaries
-  , PaymentSummary(..)
+  , paymentSummariesIndividualNonBusiness
+  , paymentSummariesForeignEmployment
+  , paymentSummariesBusinessAndPersonalServicesIncome
+  , paymentSummariesWithholdingWhereABNNotQuoted
   , paymentSummaries
-  , ABN
 
   -- *** Interest
   , interest
@@ -70,6 +66,7 @@
   , ESSStatement
   , newESSStatement
   , ess
+  , essEmployerDetails
   , essTaxedUpfrontReduction
   , essTaxedUpfrontNoReduction
   , essDeferral
@@ -124,6 +121,16 @@
   , otherDeductions
   , foreignIncomeDeductions
 
+  -- *** Deduction methods
+  , HasCentsPerKilometreMethod
+  , applyCentsPerKilometreMethod
+  , HasFixedRateMethod
+  , applyFixedRateMethod
+  , HasFixedRateMethodPre2023
+  , applyFixedRateMethodPre2023
+  , HasShortcutMethod
+  , applyShortcutMethod
+
   -- ** Tax offsets
   , Offsets
   , offsets
@@ -136,34 +143,50 @@
   , assessTax
   , taxBalance
   , taxDue
+  , offsetForSuperannuationContributionsOnBehalfOfYourSpouse
+  , foreignIncomeTaxOffsets
+  , frankingCreditOffset
   , medicareLevyDue
+  , medicareLevySurchargeDue
   , studyAndTrainingLoanRepayment
-  , taxCreditsAndOffsets
   , paygInstalmentsCredit
   , taxCGTAssessment
   , privateHealthInsuranceRebateAdjustment
 
+  -- *** Division 293
+  , division293Income
+
+  -- *** PAYG instalments
+  , paygInstalmentIncome
+
   -- * Corporate tax
   , corporateTax
 
   -- * Miscellaneous
   , GrossAndWithheld(..)
+  , HasTaxableIncome(..)
   , HasTaxWithheld(..)
   , Proportion
   , getProportion
   , proportion
   , module Data.Tax
+  , module Data.Tax.ATO.ABN
+  , module Data.Tax.ATO.Depreciation
+  , module Data.Tax.ATO.PaymentSummary
   , module Data.Tax.ATO.PrivateHealthInsuranceRebate
   , module Data.Tax.ATO.Rounding
   ) where
 
-import Control.Lens (Getter, Lens', (&), foldOf, lens, set, to, view, views)
+import Control.Lens (Getter, Lens', (&), foldOf, lens, preview, set, to, view, views)
 import Data.Time (Day)
 
 import Data.Tax
 import Data.Tax.ATO.CGT
+import Data.Tax.ATO.ABN (ABN)
 import Data.Tax.ATO.Common
+import Data.Tax.ATO.Depreciation
 import Data.Tax.ATO.FY
+import Data.Tax.ATO.PaymentSummary
 import Data.Tax.ATO.PrivateHealthInsuranceRebate
 import Data.Tax.ATO.Rounding
 
@@ -181,7 +204,7 @@
 import "Data.Tax.ATO.Pretty"
 
 -- Import the tables for the financial year ending 30 June 2024
-import qualified "Data.Tax.ATO.FY.FY2024" as FY
+import "Data.Tax.ATO.FY.FY2025"
 
 -- Convenience function for parsing a Data.Time.Day
 day :: String -> Day
@@ -189,19 +212,19 @@
 
 main :: IO ()
 main = do
-  let assessment = 'assessTax' FY.tables taxReturn
+  let assessment = 'assessTax' tables taxReturn
   putStrLn . render $ 'Data.Tax.ATO.Pretty.summariseTaxReturnInfo' taxReturn
   putStrLn ""
   putStrLn . render $ 'Data.Tax.ATO.Pretty.summariseAssessment' assessment
 
-taxReturn :: 'TaxReturnInfo' FY.FY Rational
+taxReturn :: 'TaxReturnInfo' FY Rational
 taxReturn = 'newTaxReturnInfo'
-  & set 'paymentSummaries'
-      [ 'PaymentSummary'
-          "1234567890"      -- ABN
-          (Money $ 180000)  -- Gross payments
-          (Money 50000)     -- Tax withheld
-          mempty            -- Reportable super contributions
+  & set 'paymentSummariesIndividualNonBusiness'
+      [ 'newPaymentSummaryIndividualNonBusiness' \"53 004 085 616\"  -- ABN
+          & set 'grossPayments'                                       (Money 180000)
+          & set 'totalTaxWithheld'                                    (Money  50000)
+          & set 'reportableEmployerSuperannuationContributions'       (Money   3000)
+          & set 'reportableFringeBenefits' ('fringeBenefitsEmployerNotExempt' $ Money 7274)
       ]
 
   & set 'cgtEvents'
@@ -239,20 +262,12 @@
           "MBF" "98765432" (Money 250) (Money  60) 'BenefitCode31'
       ]
 
-  & set ('deductions' . 'workRelatedTravelExpenses') (Money 1000)
+  & set ('deductions' . 'workRelatedCarExpenses') ('applyCentsPerKilometreMethod' @FY 3333)
   & set ('deductions' . 'personalSuperannuationContributions') (Money 5000)
 @
 
 -}
 
--- | Data that can have an amount of tax withheld
-class HasTaxWithheld a b c where
-  taxWithheld :: Getter (a b) (Money c)
-
-instance (Foldable t, HasTaxWithheld x a a, Num a)
-            => HasTaxWithheld t (x a) a where
-  taxWithheld = to (foldMap (view taxWithheld))
-
 -- TODO part year spouse
 newtype SpouseDetails a = SpouseDetails
   { _spouseTaxableIncome :: Money a
@@ -301,46 +316,60 @@
 --
 -- The following lenses are available:
 --
--- +---------------------------------------+----------------------------------+
--- | 'mlsExemption'                        | Medicare levy exemption          |
--- +---------------------------------------+----------------------------------+
--- | 'helpBalance'                         | HELP, VSL, SSL, ABSTUDY SSL,     |
--- |                                       | and TSL account balance          |
--- +---------------------------------------+----------------------------------+
--- | 'sfssBalance'                         | SFSS account balance             |
--- +---------------------------------------+----------------------------------+
--- | 'paymentSummaries'                    | PAYG payment summaries           |
--- +---------------------------------------+----------------------------------+
--- | 'interest'                            | Interest income and tax withheld |
--- +---------------------------------------+----------------------------------+
--- | 'dividends'                           | Dividend data                    |
--- +---------------------------------------+----------------------------------+
--- | 'ess'                                 | Employee Share Scheme statement  |
--- +---------------------------------------+----------------------------------+
--- | 'foreignIncome'                       | Foreign income                   |
--- +---------------------------------------+----------------------------------+
--- | 'cgtEvents'                           | Capital gains and losses         |
--- +---------------------------------------+----------------------------------+
--- | 'deductions'                          | Deductions                       |
--- +---------------------------------------+----------------------------------+
--- | 'offsets'                             | Tax offsets                      |
--- +---------------------------------------+----------------------------------+
--- | 'privateHealthInsurancePolicyDetails' | Private health insurance         |
--- |                                       | policy details                   |
--- +---------------------------------------+----------------------------------+
--- | 'spouseDetails'                       | Spouse Details (or @Nothing@)    |
--- +---------------------------------------+----------------------------------+
--- | 'incomeTests'                         | Income Tests                     |
--- +---------------------------------------+----------------------------------+
+-- +------------------------------------------------------+----------------------------------+
+-- | 'mlsExemption'                                       | Medicare levy exemption          |
+-- +------------------------------------------------------+----------------------------------+
+-- | 'helpBalance'                                        | HELP, VSL, SSL, ABSTUDY SSL,     |
+-- |                                                      | and AASL/TSL account balance     |
+-- +------------------------------------------------------+----------------------------------+
+-- | 'sfssBalance'                                        | SFSS account balance             |
+-- +------------------------------------------------------+----------------------------------+
+-- | 'paymentSummariesIndividualNonBusiness'              | PAYG payment summaries -         |
+-- |                                                      | individual non-business          |
+-- +------------------------------------------------------+----------------------------------+
+-- | 'paymentSummariesForeignEmployment'                  | PAYG payment summaries -         |
+-- |                                                      | foreign employment               |
+-- +------------------------------------------------------+----------------------------------+
+-- | 'paymentSummariesBusinessAndPersonalServicesIncome'  | PAYG payment summaries -         |
+-- |                                                      | business and personal services   |
+-- |                                                      | income                           |
+-- +------------------------------------------------------+----------------------------------+
+-- | 'paymentSummariesWithholdingWhereABNNotQuoted'       | PAYG payment summaries -         |
+-- |                                                      | withholding where ABN not quoted |
+-- +------------------------------------------------------+----------------------------------+
+-- | 'interest'                                           | Interest income and tax withheld |
+-- +------------------------------------------------------+----------------------------------+
+-- | 'dividends'                                          | Dividend data                    |
+-- +------------------------------------------------------+----------------------------------+
+-- | 'ess'                                                | Employee Share Scheme statement  |
+-- +------------------------------------------------------+----------------------------------+
+-- | 'foreignIncome'                                      | Foreign income                   |
+-- +------------------------------------------------------+----------------------------------+
+-- | 'cgtEvents'                                          | Capital gains and losses         |
+-- +------------------------------------------------------+----------------------------------+
+-- | 'deductions'                                         | Deductions                       |
+-- +------------------------------------------------------+----------------------------------+
+-- | 'offsets'                                            | Tax offsets                      |
+-- +------------------------------------------------------+----------------------------------+
+-- | 'privateHealthInsurancePolicyDetails'                | Private health insurance         |
+-- |                                                      | policy details                   |
+-- +------------------------------------------------------+----------------------------------+
+-- | 'spouseDetails'                                      | Spouse Details (or @Nothing@)    |
+-- +------------------------------------------------------+----------------------------------+
+-- | 'incomeTests'                                        | Income Tests                     |
+-- +------------------------------------------------------+----------------------------------+
 --
 data TaxReturnInfo y a = TaxReturnInfo
   { _mlsExemption :: Days y
   , _helpBalance :: Money a
   , _sfssBalance :: Money a
-  , _paymentSummaries :: [PaymentSummary a]
+  , _paymentSummariesIndividualNonBusiness :: [PaymentSummaryIndividualNonBusiness a]
+  , _paymentSummariesForeignEmployment :: [PaymentSummaryForeignEmployment a]
+  , _paymentSummariesBusinessAndPersonalServicesIncome :: [PaymentSummaryBusinessAndPersonalServicesIncome a]
+  , _paymentSummariesWithholdingWhereABNNotQuoted :: [PaymentSummaryWithholdingWhereABNNotQuoted a]
   , _interest :: GrossAndWithheld a
   , _dividends :: [Dividend a]
-  , _ess :: ESSStatement a
+  , _ess :: [ESSStatement a]
   , _foreignIncome :: Money a
   , _cgtEvents :: [CGTEvent a]
   , _deductions :: Deductions a
@@ -366,7 +395,10 @@
   daysAll  -- MLS exemption
   mempty -- HELP
   mempty -- SFSS
-  mempty -- payment summaries
+  [] -- payment summaries - individual non-business
+  [] -- payment summaries - foreign employment
+  [] -- payment summaries - business and personal services income
+  [] -- payment summaries - withholding where ABN not quoted
   mempty -- interest
   mempty -- dividends
   mempty -- ESS
@@ -408,16 +440,34 @@
 mlsExemption :: Lens' (TaxReturnInfo y a) (Days y)
 mlsExemption = lens _mlsExemption (\s b -> s { _mlsExemption = b })
 
-paymentSummaries :: Lens' (TaxReturnInfo y a) [PaymentSummary a]
-paymentSummaries = lens _paymentSummaries (\s b -> s { _paymentSummaries = b })
+paymentSummariesIndividualNonBusiness :: Lens' (TaxReturnInfo y a) [PaymentSummaryIndividualNonBusiness a]
+paymentSummariesIndividualNonBusiness =
+  lens _paymentSummariesIndividualNonBusiness (\s b -> s { _paymentSummariesIndividualNonBusiness = b })
 
+paymentSummariesForeignEmployment :: Lens' (TaxReturnInfo y a) [PaymentSummaryForeignEmployment a]
+paymentSummariesForeignEmployment =
+  lens _paymentSummariesForeignEmployment (\s b -> s { _paymentSummariesForeignEmployment = b })
+
+paymentSummariesBusinessAndPersonalServicesIncome :: Lens' (TaxReturnInfo y a) [PaymentSummaryBusinessAndPersonalServicesIncome a]
+paymentSummariesBusinessAndPersonalServicesIncome =
+  lens _paymentSummariesBusinessAndPersonalServicesIncome (\s b -> s { _paymentSummariesBusinessAndPersonalServicesIncome = b })
+
+paymentSummariesWithholdingWhereABNNotQuoted :: Lens' (TaxReturnInfo y a) [PaymentSummaryWithholdingWhereABNNotQuoted a]
+paymentSummariesWithholdingWhereABNNotQuoted =
+  lens _paymentSummariesWithholdingWhereABNNotQuoted (\s b -> s { _paymentSummariesWithholdingWhereABNNotQuoted = b })
+
+-- | Deprecated synonym for 'paymentSummariesIndividualNonBusiness'
+paymentSummaries :: Lens' (TaxReturnInfo y a) [PaymentSummaryIndividualNonBusiness a]
+paymentSummaries = paymentSummariesIndividualNonBusiness
+{-# DEPRECATED paymentSummaries "use 'paymentSummariesIndividualNonBusiness'" #-}
+
 interest :: Lens' (TaxReturnInfo y a) (GrossAndWithheld a)
 interest = lens _interest (\s b -> s { _interest = b })
 
 dividends :: Lens' (TaxReturnInfo y a) [Dividend a]
 dividends = lens _dividends (\s b -> s { _dividends = b })
 
-ess :: Lens' (TaxReturnInfo y a) (ESSStatement a)
+ess :: Lens' (TaxReturnInfo y a) [ESSStatement a]
 ess = lens _ess (\s b -> s { _ess = b })
 
 foreignIncome :: Lens' (TaxReturnInfo y a) (Money a)
@@ -448,9 +498,12 @@
 data TaxAssessment a = TaxAssessment
   { _taxableIncome :: Money a
   , _taxDue :: Money a
+  , _taSpouseContributionOffset :: Money a
+  , _taForeignIncomeTaxOffset :: Money a
+  , _taFrankingCreditOffset :: Money a
   , _medicareLevyDue :: Money a
+  , _medicareLevySurchargeDue :: Money a
   , _taxWithheld :: Money a
-  , _taxCreditsAndOffsets :: Money a
   , _taCGTAssessment :: CGTAssessment a
   , _phiAdj :: Money a
   , _studyAndTrainingLoanRepayment :: Money a
@@ -463,15 +516,29 @@
 instance HasTaxWithheld TaxAssessment a a where
   taxWithheld = to _taxWithheld
 
-taxDue :: Getter (TaxAssessment a) (Money a)
-taxDue = to _taxDue
+taxDue :: Lens' (TaxAssessment a) (Money a)
+taxDue = lens _taxDue (\s b -> s { _taxDue = b })
 
-medicareLevyDue :: Getter (TaxAssessment a) (Money a)
-medicareLevyDue = to _medicareLevyDue
+offsetForSuperannuationContributionsOnBehalfOfYourSpouse :: Lens' (TaxAssessment a) (Money a)
+offsetForSuperannuationContributionsOnBehalfOfYourSpouse =
+  lens _taSpouseContributionOffset (\s b -> s { _taSpouseContributionOffset = b })
 
-taxCreditsAndOffsets :: Getter (TaxAssessment a) (Money a)
-taxCreditsAndOffsets = to _taxCreditsAndOffsets
+foreignIncomeTaxOffsets :: Lens' (TaxAssessment a) (Money a)
+foreignIncomeTaxOffsets =
+  lens _taForeignIncomeTaxOffset (\s b -> s { _taForeignIncomeTaxOffset = b })
 
+frankingCreditOffset :: Lens' (TaxAssessment a) (Money a)
+frankingCreditOffset =
+  lens _taFrankingCreditOffset (\s b -> s { _taFrankingCreditOffset = b })
+
+medicareLevyDue :: Lens' (TaxAssessment a) (Money a)
+medicareLevyDue =
+  lens _medicareLevyDue (\s b -> s { _medicareLevyDue = b })
+
+medicareLevySurchargeDue :: Lens' (TaxAssessment a) (Money a)
+medicareLevySurchargeDue =
+  lens _medicareLevySurchargeDue (\s b -> s { _medicareLevySurchargeDue = b })
+
 taxCGTAssessment :: Lens' (TaxAssessment a) (CGTAssessment a)
 taxCGTAssessment = lens _taCGTAssessment (\s b -> s { _taCGTAssessment = b })
 
@@ -488,17 +555,27 @@
 
 -- | What is the balance of the assessment?  Positive means a
 -- refund (tax withheld exceeds obligation), negative means a bill.
-taxBalance :: Num a => Getter (TaxAssessment a) (Money a)
-taxBalance = to $ \a ->
-  view taxWithheld a
-  $-$ view taxDue a
-  $-$ view medicareLevyDue a
-  $-$ view studyAndTrainingLoanRepayment a
-  $-$ view privateHealthInsuranceRebateAdjustment a
-  $+$ view taxCreditsAndOffsets a
-  $+$ view paygInstalmentsCredit a
+taxBalance :: (Num a, Ord a) => Getter (TaxAssessment a) (Money a)
+taxBalance = to $ fmap negate . \a ->
+  max (Money 0) (
+    -- tax on taxable income
+    view taxDue a
+    -- less non-refundable offsets
+    $-$ view offsetForSuperannuationContributionsOnBehalfOfYourSpouse a
+    $-$ view foreignIncomeTaxOffsets a
+  )
+  -- less refundable tax offsets
+  $-$ view frankingCreditOffset a
+  -- plus other liabilities
+  $+$ view medicareLevyDue a
+  $+$ view medicareLevySurchargeDue a
+  $+$ view privateHealthInsuranceRebateAdjustment a
+  $+$ view studyAndTrainingLoanRepayment a
+  -- less PAYG credits and other entitlements
+  $-$ view taxWithheld a
+  $-$ view paygInstalmentsCredit a
 
-instance (Num a, Eq a) => HasCapitalLossCarryForward TaxAssessment a where
+instance HasCapitalLossCarryForward TaxAssessment a where
   capitalLossCarryForward = taxCGTAssessment . capitalLossCarryForward
 
 
@@ -523,31 +600,16 @@
   limit (view helpBalance info) (ttHelp table)
   <> limit (view sfssBalance info) (ttSfss table)
 
--- | Medicare levy + surcharge
-medicareLevyTax
-  :: (FinancialYear y, Fractional a)
-  => TaxTables y a
-  -> TaxReturnInfo y a
-  -> Tax (Money a) (Money a)    -- grand unified individual income tax
-medicareLevyTax table info =
-  let
-    ml = ttMedicareLevy table
-    mls = ttMedicareLevySurcharge table
-    mlsFrac = 1 - getFraction (view mlsExemption info)
-  in
-    -- TODO medicare levy exemption
-    ml
-    -- FIXME income for MLS purposes includes
-    -- fringe benefits; family thresholds apply
-    <> fmap ($* mlsFrac) mls
-
 -- | Taxable income
 instance (RealFrac a) => HasTaxableIncome (TaxReturnInfo y) a a where
   taxableIncome = to $ \info ->
     let
       cf = view capitalLossCarryForward info
       gross = foldMap wholeDollars
-        [ view (paymentSummaries . taxableIncome) info
+        [ view (paymentSummariesIndividualNonBusiness . taxableIncome) info
+        , view (paymentSummariesForeignEmployment . taxableIncome) info
+        , view (paymentSummariesBusinessAndPersonalServicesIncome . taxableIncome) info
+        , view (paymentSummariesWithholdingWhereABNNotQuoted . taxableIncome) info
         , view (interest . taxableIncome) info
         , view (dividends . taxableIncome) info
         , view (ess . taxableIncome) info
@@ -557,13 +619,67 @@
     in
       wholeDollars (gross $-$ views deductions totalDeductions info)
 
--- | Includes PAYG withholding by employer or bank.
--- Does not include franking credits.
+-- | Includes PAYG withholding by employer, and TFN
+-- amounts withheld e.g. from bank interest, ESS discounts, etc.
+--
+-- Does not include franking credits (which are a refundable offset).
+--
 instance (Num a) => HasTaxWithheld (TaxReturnInfo y) a a where
   taxWithheld = to $ \info ->
-    view (paymentSummaries . taxWithheld) info
+    view (paymentSummariesIndividualNonBusiness . taxWithheld) info
+    <> view (paymentSummariesForeignEmployment . taxWithheld) info
+    <> view (paymentSummariesBusinessAndPersonalServicesIncome . taxWithheld) info
+    <> view (paymentSummariesWithholdingWhereABNNotQuoted . taxWithheld) info
     <> view (interest . taxWithheld) info
+    <> view (ess . taxWithheld) info
 
+fringeBenefits :: (Num a) => TaxReturnInfo y a -> Money a
+fringeBenefits info =
+  foldOf l'inb info
+  <> foldOf l'foreign info
+  where
+    l'inb =
+      paymentSummariesIndividualNonBusiness . traverse
+      . reportableFringeBenefits . traverse
+      . to (\(ReportableFringeBenefits amount _) -> amount)
+    l'foreign =
+      paymentSummariesForeignEmployment . traverse
+      . reportableFringeBenefits . traverse
+      . to (\(ReportableFringeBenefits amount _) -> amount)
+
+reportableSuperContributions :: (Num a) => TaxReturnInfo y a -> Money a
+reportableSuperContributions info =
+  foldOf (paymentSummariesIndividualNonBusiness . traverse . reportableEmployerSuperannuationContributions) info
+  <> foldOf (paymentSummariesForeignEmployment . traverse . reportableEmployerSuperannuationContributions) info
+  <> foldOf (paymentSummariesBusinessAndPersonalServicesIncome . traverse . reportableEmployerSuperannuationContributions) info
+  <> view (deductions . personalSuperannuationContributions) info
+
+-- | Calculate Division 293 income
+division293Income :: (RealFrac a) => TaxReturnInfo y a -> Money a
+division293Income info =
+  view taxableIncome info
+  <> fringeBenefits info
+  -- TODO net financial investment loss
+  -- TODO net rental property loss
+  -- TODO net amount on which family trust distribution has been paid
+  -- TODO super lump sum taxed elements with zero tax rate
+  -- TODO assessable FHSS released amount
+
+paygInstalmentIncome :: (RealFrac a) => TaxReturnInfo y a -> Money a
+paygInstalmentIncome info =
+  -- TODO gross rent
+  view (dividends . taxableIncome) info
+  -- TODO royalties?
+  -- TODO foreign pensions assessable in Australia
+  -- TODO share of income from partnerships and trusts
+  <> view foreignIncome info
+  <> view (interest . taxableIncome) info
+  -- TODO business income (incl PSI etc)
+  <> view (paymentSummariesWithholdingWhereABNNotQuoted . taxableIncome) info
+  -- TODO gross income where tax withheld due to not provide TFN
+  -- TODO withdrawal from farm management deposits
+  -- TODO fuel tax credits
+
 -- | Assess a tax return, given tax tables and tax return info.
 assessTax
   :: (FinancialYear y, RealFrac a)
@@ -574,20 +690,47 @@
           (view capitalLossCarryForward info) (view cgtEvents info)
     taxable = view taxableIncome info
     due = getTax (individualTax tables) taxable
-    studyRepayment = getTax (studyAndTrainingLoanRepaymentTax tables info) taxable
-    mlAndMLS = getTax (medicareLevyTax tables info) taxable
 
-    incomeForSurchargePurposes =
+    exemptForeignIncome =
+      foldOf (paymentSummariesIndividualNonBusiness . traverse . exemptForeignEmploymentIncome) info
+
+    repaymentIncome =
       taxable
-      -- TODO reportable fringe benefits
-      -- TODO net investment losses
-      <> foldOf (paymentSummaries . traverse . to reportableEmployerSuperannuationContributions) info
+      <> fringeBenefits info
+      -- TODO net financial investment losses
+      -- TODO net rental property losses
+      <> reportableSuperContributions info
+      <> exemptForeignIncome
 
+    studyRepayment = getTax (studyAndTrainingLoanRepaymentTax tables info) repaymentIncome
+
+    ml = medicareLevy'
+          (ttMedicareLevyRatesAndThresholds tables)
+          taxable
+          (preview (spouseDetails . traverse . spouseTaxableIncome) info)
+          (view (incomeTests . dependentChildren) info)
+
+    surchargeIncome =
+      taxable
+      <> fringeBenefits info
+      -- TODO net financial investment losses
+      -- TODO net rental property losses
+      <> reportableSuperContributions info
+      -- TODO spouse's share of net income of a trust on which the trustee
+      --      must pay tax, if not included in taxable income
+      <> ( if taxable > mempty then exemptForeignIncome else mempty )
+      -- TODO reduction by taxed element of super lump sum
+
+    mls =
+      let mlsFrac = 1 - getFraction (view mlsExemption info)
+      -- TODO family thresholds apply
+      in getTax (fmap ($* mlsFrac) (ttMedicareLevySurcharge tables)) surchargeIncome
+
     spouseIncomeForSurchargePurposes =
       fmap (view spouseTaxableIncome) (view spouseDetails info)
 
     phiAdj = assessExcessPrivateHealthRebate
-      incomeForSurchargePurposes
+      surchargeIncome
       spouseIncomeForSurchargePurposes
       (view (incomeTests . dependentChildren) info)
       (ttPHIRebateRates tables)
@@ -595,7 +738,7 @@
 
     foreignIncomeTaxOffsetLimit =
       let
-        step1 = due <> mlAndMLS
+        step1 = due <> ml <> mls
         step2 =
           let
             info' = info & set foreignIncome mempty
@@ -603,48 +746,41 @@
               view taxableIncome info'
               <> view (deductions . foreignIncomeDeductions) info'
             due' = getTax (individualTax tables) taxable'
-            mlAndMLS' = getTax (medicareLevyTax tables info') taxable'
+            ml' = medicareLevy'
+                    (ttMedicareLevyRatesAndThresholds tables)
+                    taxable
+                    (preview (spouseDetails . traverse . spouseTaxableIncome) info)
+                    (view (incomeTests . dependentChildren) info)
+            mls' =
+              let mlsFrac = 1 - getFraction (view mlsExemption info)
+              -- FIXME income for MLS purposes includes fringe benefits; family thresholds apply
+              in getTax (fmap ($* mlsFrac) (ttMedicareLevySurcharge tables)) taxable
           in
-            due' <> mlAndMLS'
+            due' <> ml' <> mls'
         step3 = step1 $-$ step2
       in
         max (Money 1000) step3
 
     frankingCredit = wholeDollars $ view (dividends . taxWithheld) info
-    off =
-      view (offsets . spouseContributionOffset) info
-      <> min (view (offsets . foreignTaxOffset) info) foreignIncomeTaxOffsetLimit
+    spouseContribOffset = view (offsets . spouseContributionOffset) info
+    fito = min (view (offsets . foreignTaxOffset) info) foreignIncomeTaxOffsetLimit
 
   in
     TaxAssessment
       taxable
       due
-      mlAndMLS
+      spouseContribOffset
+      fito
+      frankingCredit
+      ml
+      mls
       (view taxWithheld info)
-      (frankingCredit <> off)
       cg
       phiAdj
       studyRepayment
       (view (offsets . paygInstalments) info)
 
--- | Australian Business Number
-type ABN = String
 
--- | PAYG payment summary - individual non-business
-data PaymentSummary a = PaymentSummary
-  { summaryABN :: ABN
-  , summaryGross :: Money a
-  , summaryWithheld :: Money a
-  , reportableEmployerSuperannuationContributions :: Money a
-  }
-
--- | Gross income
-instance HasTaxableIncome PaymentSummary a a where
-  taxableIncome = to summaryGross
-
-instance HasTaxWithheld PaymentSummary a a where
-  taxWithheld = to summaryWithheld
-
 -- | A proportion is a non-negative number in interval @[0,1]@.
 -- Use 'proportion' to construct.
 newtype Proportion a = Proportion
@@ -991,6 +1127,8 @@
 -- The following lenses are available:
 --
 -- +------------------------------+--------------------------------------------+
+-- | 'essEmployerDetails'         | 'PayerDetails' for employer.               |
+-- +------------------------------+--------------------------------------------+
 -- | 'essTaxedUpfrontReduction'   | __D__ Discount from taxed up front         |
 -- |                              | schemes—eligible for reduction             |
 -- +------------------------------+--------------------------------------------+
@@ -1009,18 +1147,24 @@
 -- +------------------------------+--------------------------------------------+
 --
 data ESSStatement a = ESSStatement
-  { _taxedUpfrontReduction :: Money a
+  { _essPayer :: PayerDetails
+  , _taxedUpfrontReduction :: Money a
   , _taxedUpfrontNoReduction :: Money a
   , _deferral :: Money a
   , _pre2009 :: Money a
   , _tfnAmounts :: Money a
   , _foreignSourceDiscounts :: Money a
   }
+  deriving (Eq, Ord)
 
 -- | Construct an 'ESSStatement' with all amounts at /zero/.
-newESSStatement :: Num a => ESSStatement a
-newESSStatement = ESSStatement mempty mempty mempty mempty mempty mempty
+newESSStatement :: (Num a) => PayerDetails -> ESSStatement a
+newESSStatement payer = ESSStatement payer mempty mempty mempty mempty mempty mempty
 
+-- | Employer details
+essEmployerDetails :: Lens' (ESSStatement a) PayerDetails
+essEmployerDetails = lens _essPayer (\s b -> s { _essPayer = b })
+
 -- | Discount from taxed up front schemes—eligible for reduction.
 -- Item __D__ in /Employee share schemes/ section.
 essTaxedUpfrontReduction :: Lens' (ESSStatement a) (Money a)
@@ -1064,10 +1208,5 @@
     <> view essDeferral s
     <> view essPre2009 s
 
-instance (Num a) => Semigroup (ESSStatement a) where
-  ESSStatement a b c d e f <> ESSStatement a' b' c' d' e' f' =
-    ESSStatement (a <> a') (b <> b') (c <> c') (d <> d') (e <> e') (f <> f')
-
-instance (Num a) => Monoid (ESSStatement a) where
-  mempty = newESSStatement
-  mappend = (<>)
+instance HasTaxWithheld ESSStatement a a where
+  taxWithheld = essTFNAmounts
diff --git a/src/Data/Tax/ATO/ABN.hs b/src/Data/Tax/ATO/ABN.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Tax/ATO/ABN.hs
@@ -0,0 +1,94 @@
+-- This file is part of hs-tax-ato
+-- Copyright (C) 2025  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/>.
+
+{-|
+
+Australian Business Number (ABN) types and functions.
+
+@
+import qualified Data.Tax.ATO.ABN as ABN
+
+main = do
+  let
+    abnStr = "53 004 085 616"
+    Just abn = ABN.parse abnStr
+    abnInt = ABN.asInt abn
+    Just abn' = ABN.fromInt abnInt
+  putStrLn $ ABN.asString abn'  -- prints "53 004 085 616"
+@
+
+It is recommended to import this module qualified.
+
+You can use the @OverloadedStrings@ language extension or the 'parse' function
+to convert string to ABNs.  Space characters are ignored, but other non-digits
+will cause parse failure.
+
+-}
+
+module Data.Tax.ATO.ABN
+  (
+    ABN
+  , parse
+  , fromInt
+  , asString
+  , asInt
+  ) where
+
+import Data.Char (isDigit)
+import Data.String
+import Data.Maybe (fromJust)
+
+-- | Australian Business Number (ABN)
+newtype ABN = ABN Int
+  deriving (Eq, Ord, Show)
+
+-- | Parse an ABN, throwing an error if it is not valid.
+instance IsString ABN where
+  fromString = fromJust . parse
+
+-- | Construct an ABN, checking validity.
+fromInt :: Int -> Maybe ABN
+fromInt n | valid n = Just (ABN n)
+fromInt _           = Nothing
+
+-- | Parse an ABN.  Spaces characters are allowed, other non-digit
+-- characters are not.
+parse :: String -> Maybe ABN
+parse s
+  | all isDigit s'  = fromInt (read s')
+  | otherwise       = Nothing
+  where s' = filter (/= ' ') s
+
+valid :: Int -> Bool
+valid n =
+  (== 0) . (`mod` 89) . subtract 10 . snd $ foldr step (n, 0) weights
+  where
+    weights = [10,1,3,5,7,9,11,13,15,17,19]
+    step weight (n',acc) = ((+ acc) . (* weight)) <$> divMod n' 10
+
+asInt :: ABN -> Int
+asInt (ABN n) = n
+
+-- Format ABN in the standard way (XX XXX XXX XXX)
+asString :: ABN -> String
+asString (ABN n) =
+  case s' of
+    [a1,a2,b1,b2,b3,c1,c2,c3,d1,d2,d3]  -> [a1,a2,' ',b1,b2,b3,' ',c1,c2,c3,' ',d1,d2,d3]
+    _                                   -> show n -- can't happen, but just in case...
+  where
+    n' = abs n  -- negative can't happen, but just in case...
+    s = show n'
+    s' = replicate (11 - length s) '0' <> s  -- left-pad to 11 digits
diff --git a/src/Data/Tax/ATO/CGT.hs b/src/Data/Tax/ATO/CGT.hs
--- a/src/Data/Tax/ATO/CGT.hs
+++ b/src/Data/Tax/ATO/CGT.hs
@@ -27,9 +27,6 @@
 
 -}
 
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE MultiParamTypeClasses #-}
-
 module Data.Tax.ATO.CGT
   (
   -- * CGT events
@@ -37,11 +34,12 @@
 
   -- * CGT assessments for tax returns
   , assessCGTEvents
-  , CGTAssessment(CGTAssessment)
-  , CGTNetGainOrLoss(..)
+  , CGTAssessment
+  , nullCGTAssessment
   , HasCapitalLossCarryForward(..)
-  , cgtNetGainOrLoss
   , cgtNetGain
+  , cgtTotalCurrentYearGains
+  , cgtNetLossesCarriedForward
 
   -- * CGT computations
   , HasCapitalGain(..)
@@ -49,7 +47,6 @@
   , isCapitalGain
   , isCapitalLoss
   , discountApplicable
-  , netCapitalGainOrLoss
   ) where
 
 import Data.Foldable (toList)
@@ -139,34 +136,6 @@
   capitalGain = to (foldMap (view capitalGain))
 
 
-
--- | Compute the /discounted/ gain or carry-forward loss
---
--- Losses are used to offset non-discountable capital gains
--- first, then discountable gains, before the discount is applied
--- to discountable gains.
---
--- *Does not implement the indexation method for cost-base reduction!*
---
-netCapitalGainOrLoss
-  :: (Fractional a, Ord a, Foldable t)
-  => Money a                     -- ^ loss carried forward
-  -> t (CGTEvent a)              -- ^ CGT events
-  -> CGTNetGainOrLoss a
-netCapitalGainOrLoss carry events =
-  let
-    l = toList events
-    (discountableGain, nonDiscountableGain) =
-      over both (view capitalGain) (partition discountApplicable l)
-    loss = foldMap capitalLoss l
-    (nonDiscLessLoss, remLoss) = sub nonDiscountableGain (loss <> carry)
-    (discLessLoss, finalLoss) = sub discountableGain remLoss
-    discGain = nonDiscLessLoss <> (discLessLoss $* 0.5)
-  in
-    if discGain > mempty
-    then CGTNetGain discGain
-    else CGTLoss finalLoss
-
 -- | @sub x y@ = subtract @y@ from @x@, clamping to 0 and
 -- returning @(result, leftovers)@
 --
@@ -176,56 +145,88 @@
   in (max mempty r, over money abs (min mempty r))
 
 -- | Assess the total capital gains and net capital gain or loss.
+--
+-- Losses are used to offset non-discountable capital gains
+-- first, then discountable gains, before the discount is applied
+-- to discountable gains.
+--
+-- __Does not implement the indexation method for cost-base reduction.__
+--
 assessCGTEvents
   :: (Fractional a, Ord a, Foldable t)
   => Money a            -- ^ capital loss carried forward
   -> t (CGTEvent a)
   -> CGTAssessment a
-assessCGTEvents carry evs = CGTAssessment
-  (view capitalGain evs)
-  (netCapitalGainOrLoss carry evs)
+assessCGTEvents carry events =
+  let
+    l = toList events
+    (discountableGain, nonDiscountableGain) =
+      over both (view capitalGain) (partition discountApplicable l)
+    totalGain = discountableGain <> nonDiscountableGain
+    totalLoss = foldMap capitalLoss l
+    (nonDiscountableGainLossesApplied, unappliedLosses) = sub nonDiscountableGain (totalLoss <> carry)
+    (discountableGainLossesApplied, finalUnappliedLosses) = sub discountableGain unappliedLosses
+    discount = discountableGainLossesApplied $* 0.5
+    discountedGain = nonDiscountableGainLossesApplied <> (discountableGainLossesApplied $-$ discount)
 
+    lossesApplied = (totalLoss <> carry) $-$ finalUnappliedLosses
+    priorYearLossesApplied = min carry lossesApplied
+    currentYearLossesApplied = lossesApplied $-$ priorYearLossesApplied
+  in
+    CGTAssessment
+      totalGain
+      totalLoss
+      currentYearLossesApplied
+      priorYearLossesApplied
+      finalUnappliedLosses
+      discount
+      discountedGain
+
 -- | Total undiscounted gains and net gain/loss for tax assessment
 data CGTAssessment a = CGTAssessment
-  { _cgtaTotal :: Money a
-  , _cgtaNet :: CGTNetGainOrLoss a
+  { _totalCurrentYearCapitalGains :: Money a
+  , _totalCurrentYearCapitalLosses :: Money a
+  , _totalCurrentYearCapitalLossesApplied :: Money a
+  , _totalPriorYearCapitalLossesApplied :: Money a
+  , _netCapitalLossesCarriedForward :: Money a
+  , _totalCGTDiscountApplied :: Money a
+  , _netCapitalGain :: Money a
   }
-  deriving (Show)
-
-instance Functor CGTAssessment where
-  fmap f (CGTAssessment a b) = CGTAssessment (fmap f a) (fmap f b)
+  deriving (Eq)
 
-instance HasCapitalGain CGTAssessment a a where
-  capitalGain = to _cgtaTotal
+-- | A 'CGTAssessment' whose values are all zero
+nullCGTAssessment :: (Num a) => CGTAssessment a
+nullCGTAssessment = CGTAssessment mempty mempty mempty mempty mempty mempty mempty
 
--- | The 'CGTNetGainOrLoss' value of the 'CGTAssessment'
-cgtNetGainOrLoss :: Lens' (CGTAssessment a) (CGTNetGainOrLoss a)
-cgtNetGainOrLoss = lens _cgtaNet (\s b -> s { _cgtaNet = b })
+instance Functor CGTAssessment where
+  fmap f (CGTAssessment a b c d e g h) = CGTAssessment
+    (fmap f a)
+    (fmap f b)
+    (fmap f c)
+    (fmap f d)
+    (fmap f e)
+    (fmap f g)
+    (fmap f h)
 
--- | The net capital gain, or zero if a loss.
-cgtNetGain :: (Num a) => Getter (CGTAssessment a) (Money a)
-cgtNetGain = cgtNetGainOrLoss . to f
-  where
-  f (CGTNetGain a) = a
-  f _ = mempty
+-- | __18A__ The net capital gain, or zero if a loss.
+cgtNetGain :: Lens' (CGTAssessment a) (Money a)
+cgtNetGain = lens _netCapitalGain (\s b -> s { _netCapitalGain = b })
 
--- | A net (loss offset, discounted) gain, or the loss amount
-data CGTNetGainOrLoss a = CGTNetGain (Money a) | CGTLoss (Money a)
-  deriving (Show)
+-- | __18H__ Total current year capital gains
+cgtTotalCurrentYearGains :: Lens' (CGTAssessment a) (Money a)
+cgtTotalCurrentYearGains =
+  lens _totalCurrentYearCapitalGains (\s b -> s { _totalCurrentYearCapitalGains = b })
 
-instance Functor CGTNetGainOrLoss where
-  fmap f (CGTNetGain a) = CGTNetGain (fmap f a)
-  fmap f (CGTLoss a)    = CGTLoss (fmap f a)
+-- | __18V__ Net capital losses carried forward to later income years
+cgtNetLossesCarriedForward :: Lens' (CGTAssessment a) (Money a)
+cgtNetLossesCarriedForward =
+  lens _netCapitalLossesCarriedForward (\s b -> s { _netCapitalLossesCarriedForward = b })
 
 -- | Types that have a carry-forward capital loss (either as an
 -- input or an output).
 class HasCapitalLossCarryForward a b where
   capitalLossCarryForward :: Lens' (a b) (Money b)
 
-instance (Num a, Eq a) => HasCapitalLossCarryForward CGTNetGainOrLoss a where
-  capitalLossCarryForward = lens
-    (\s -> case s of CGTLoss a -> a ; _ -> mempty)
-    (\s b -> if b == mempty then s else CGTLoss b)
-
-instance (Num a, Eq a) => HasCapitalLossCarryForward CGTAssessment a where
-  capitalLossCarryForward = cgtNetGainOrLoss . capitalLossCarryForward
+instance HasCapitalLossCarryForward CGTAssessment a where
+  capitalLossCarryForward =
+    lens _netCapitalLossesCarriedForward (\s b -> s { _netCapitalLossesCarriedForward = b })
diff --git a/src/Data/Tax/ATO/Common.hs b/src/Data/Tax/ATO/Common.hs
--- a/src/Data/Tax/ATO/Common.hs
+++ b/src/Data/Tax/ATO/Common.hs
@@ -20,9 +20,9 @@
 
 -}
 
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE MultiParamTypeClasses #-}
-{-# LANGUAGE PolyKinds #-}
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE UndecidableInstances #-}
 
 module Data.Tax.ATO.Common
   (
@@ -31,7 +31,12 @@
 
   -- * Classes
   , HasTaxableIncome(..)
+  , HasTaxWithheld(..)
 
+  -- * Medicare levy
+  , MedicareLevyRatesAndThresholds(..)
+  , medicareLevy'
+
   -- * Common taxes and helpers
   , medicareLevy
   , lowIncomeTaxOffset
@@ -39,6 +44,17 @@
   , lamito
   , corporateTax
 
+  -- * Deduction rates that vary by financial year
+  , applyCentsPerKilometreMethod
+  , HasCentsPerKilometreMethod
+
+  , applyFixedRateMethod
+  , HasFixedRateMethod
+  , applyFixedRateMethodPre2023
+  , HasFixedRateMethodPre2023
+  , applyShortcutMethod
+  , HasShortcutMethod
+
   -- * Convenience functions
   , thresholds'
   , marginal'
@@ -46,14 +62,17 @@
 
 import Control.Lens (Getter, review, to, view)
 import Data.Bifunctor (first)
+import Data.Proxy
+import GHC.TypeLits
 
 import Data.Tax
 import Data.Tax.ATO.PrivateHealthInsuranceRebate
+import Data.Tax.ATO.FY
 
 -- | A set of tax tables for a particular financial year
 data TaxTables y a = TaxTables
   { ttIndividualIncomeTax :: Tax (Money a) (Money a)
-  , ttMedicareLevy :: Tax (Money a) (Money a)
+  , ttMedicareLevyRatesAndThresholds :: MedicareLevyRatesAndThresholds a
   , ttMedicareLevySurcharge :: Tax (Money a) (Money a)
   , ttHelp :: Tax (Money a) (Money a)
   , ttSfss :: Tax (Money a) (Money a)
@@ -62,6 +81,40 @@
   , ttPHIRebateRates :: PrivateHealthInsuranceRebateRates a
   }
 
+
+data MedicareLevyRatesAndThresholds a = MedicareLevyRatesAndThresholds
+  { medicareLevyRate :: a
+  , medicareLevyThresholdIndividual :: Money a
+  , medicareLevyThresholdIndividualSeniorAndPensioner :: Money a
+  , medicareLevyThresholdFamily :: Money a
+  , medicareLevyThresholdFamilySeniorAndPensioner :: Money a
+  , medicareLevyThresholdDependentChildIncrease :: Money a
+  }
+
+medicareLevy'
+  :: (Fractional a, Ord a)
+  => MedicareLevyRatesAndThresholds a
+  -> Money a          -- ^ taxable income
+  -> Maybe (Money a)  -- ^ spouse taxable income
+  -> Integer          -- ^ number of dependents
+  -> Money a
+medicareLevy' rates me mYou nDeps = case mYou of
+  Nothing | nDeps <= 0 ->
+    let
+      t = medicareLevyThresholdIndividual rates
+    in
+      getTax (mk t) me
+  _ ->
+    let
+      t = medicareLevyThresholdFamily rates
+          $+$ fromIntegral nDeps *$ medicareLevyThresholdDependentChildIncrease rates
+      combinedIncome = maybe me (me $+$) mYou
+    in
+      (me $/$ combinedIncome) *$ getTax (mk t) combinedIncome
+  where
+    mk l = lesserOf (above l 0.1) (flat (medicareLevyRate rates))
+
+
 -- | The Medicare levy, incorporating the Medicare levy reduction.
 -- The rate is 10% of the income above the given shade-in threshold
 -- or 2% of the total income, whichever is less.
@@ -116,3 +169,164 @@
     => HasTaxableIncome t (x a) a
     where
   taxableIncome = to (foldMap (view taxableIncome))
+
+-- | Types that can have an amount of tax withheld.
+class HasTaxWithheld a b c where
+  taxWithheld :: Getter (a b) (Money c)
+
+instance (Foldable t, HasTaxWithheld x a a, Num a)
+            => HasTaxWithheld t (x a) a where
+  taxWithheld = to (foldMap (view taxWithheld))
+
+
+-- | See 'applyCentsPerKilometreMethod'.
+class HasCentsPerKilometreMethod y where
+  centsPerKilometre :: Num a => a
+
+instance (2016 <= y, y <= 2025, FinancialYear y) => HasCentsPerKilometreMethod y where
+  centsPerKilometre = case fromProxy (Proxy @y) of
+    y | y == 2025           -> 88
+      | y == 2024           -> 85
+      | y == 2023           -> 78
+      | y > 2020, y <= 2022 -> 72
+      | y > 2018, y <= 2020 -> 68
+      | y > 2015, y <= 2018 -> 66
+      | otherwise           ->  0  -- can't happen
+
+-- | Apply the single-rate cents per kilometre method (in use since
+-- the 2015–16 FY).  Clamped (by law) at 5000 kms.  Use
+-- @TypeApplications@ to specify the financial year, e.g.
+--
+-- @
+-- import Data.Tax.ATO.FY.2025 (FY)
+--
+-- y = applyCentsPerKilometreMethod \@2025 1111  -- using type literal
+-- x = applyCentsPerKilometreMethod \@FY   1111  -- same thing, using type synonym
+-- @
+--
+applyCentsPerKilometreMethod
+  :: forall y a. (HasCentsPerKilometreMethod y, Fractional a, Ord a)
+  => a -> Money a
+applyCentsPerKilometreMethod kms = Money $ centsPerKilometre @y * min kms 5000 / 100
+
+
+-- | See 'applyFixedRateMethod'
+class HasFixedRateMethod y where
+  fixedRateMethodCentsPerHour :: Num a => a
+
+instance HasFixedRateMethod 2023 where
+  fixedRateMethodCentsPerHour = 67
+instance HasFixedRateMethod 2024 where
+  fixedRateMethodCentsPerHour = 67
+instance HasFixedRateMethod 2025 where
+  fixedRateMethodCentsPerHour = 70
+
+-- | The updated fixed rate method available from the 2022–23 income
+-- year.  It covers:
+--
+-- * home and mobile internet or data expenses
+-- * mobile and home phone usage expenses
+-- * electricity and gas (energy expenses) for heating, cooling and lighting
+-- * stationery and computer consumables, such as printer ink and paper.
+--
+-- You can separately claim deductions for work-related use of
+-- technology and office furniture, and (in limited circumstances)
+-- occupancy expenses and cleaning expenses.
+--
+-- Use @TypeApplications@ to specify the financial year, e.g.
+--
+-- @
+-- import Data.Tax.ATO.FY.2025 (FY)
+--
+-- y = applyFixedRateMethod \@2025 1111  -- using type literal
+-- x = applyFixedRateMethod \@FY   1111  -- same thing, using type synonym
+-- @
+--
+-- For earlier income years use 'applyFixedRateMethodPre2023'.
+--
+applyFixedRateMethod
+  :: forall y a. (HasFixedRateMethod y, Fractional a)
+  => a -> Money a
+applyFixedRateMethod hours = Money $ fixedRateMethodCentsPerHour @y * hours / 100
+
+
+-- | See 'applyShortcutMethod'
+class HasShortcutMethod y where
+  shortcutMethodCentsPerHour :: Num a => a
+
+instance (2020 <= y, y <= 2022) => HasShortcutMethod y where
+  shortcutMethodCentsPerHour = 80
+
+-- | The shortcut method for working from home deductions was
+-- available from 1 March 2020 to 30 June 2020 in the 2019–20 income
+-- year, and for the 2020–21 and 2021–22 income years.  It covers:
+--
+-- * phone and data expenses
+-- * internet expenses
+-- * the decline in value of equipment and furniture
+-- * electricity and gas (energy expenses) for heating, cooling and lighting.
+--
+-- Use @TypeApplications@ to specify the financial year, e.g.
+--
+-- @
+-- import Data.Tax.ATO.FY.2025 (FY)
+--
+-- y = applyShortcutMethod \@2025 1111  -- using type literal
+-- x = applyShortcutMethod \@FY   1111  -- same thing, using type synonym
+-- @
+--
+applyShortcutMethod
+  :: forall y a. (HasShortcutMethod y, Fractional a)
+  => a -> Money a
+applyShortcutMethod hours = Money $ shortcutMethodCentsPerHour @y * hours / 100
+
+
+-- | See 'applyFixedRateMethodPre2023'
+class HasFixedRateMethodPre2023 y where
+  fixedRateMethodPre2023CentsPerHour :: Num a => a
+
+instance (2002 <= y, y <= 2022, FinancialYear y) => HasFixedRateMethodPre2023 y where
+  fixedRateMethodPre2023CentsPerHour = case fromProxy (Proxy @y) of
+    y | y > 2018, y <= 2022 -> 52
+      | y > 2014, y <= 2018 -> 45
+      | y > 2010, y <= 2014 -> 34
+      | y > 2004, y <= 2010 -> 26
+      | y > 2001, y <= 2004 -> 20
+      | otherwise           ->  0  -- can't happen
+
+-- | The fixed rate method available up to the 2021–22 income year.
+-- It differs from the current fixed rate method (see
+-- 'HasFixedRateMethod') in which categories of expenses are
+-- included.  This method covers:
+--
+-- * the decline in value of home office furniture and
+--   furnishings – for example, a desk
+-- * electricity and gas (energy expenses) for heating,
+--   cooling and lighting
+-- * cleaning your dedicated home office.
+--
+-- You should separately claim for:
+--
+-- * home phone and internet expenses, including the decline in
+--   value of the handset
+-- * stationery and computer consumables, such as printer ink
+--   and paper
+-- * decline in value of depreciating assets other than
+--   home office furniture and furnishings used for work
+--   purposes – for example, computers and laptops.
+--
+-- Use @TypeApplications@ to specify the financial year, e.g.
+--
+-- @
+-- import Data.Tax.ATO.FY.2025 (FY)
+--
+-- y = applyFixedRateMethodPre2023 \@2025 1111  -- using type literal
+-- x = applyFixedRateMethodPre2023 \@FY   1111  -- same thing, using type synonym
+-- @
+--
+-- For later income years, use 'applyFixedRateMethod'.
+--
+applyFixedRateMethodPre2023
+  :: forall y a. (HasFixedRateMethodPre2023 y, Fractional a)
+  => a -> Money a
+applyFixedRateMethodPre2023 hours = Money $ fixedRateMethodPre2023CentsPerHour @y * hours / 100
diff --git a/src/Data/Tax/ATO/Depreciation.hs b/src/Data/Tax/ATO/Depreciation.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Tax/ATO/Depreciation.hs
@@ -0,0 +1,87 @@
+
+{-|
+
+Types and computations for calculating decline in value.
+
+-}
+
+module Data.Tax.ATO.Depreciation
+  ( DepreciatingAsset(..)
+  , EffectiveLife
+  , DepreciationMethod
+  , diminishingValueMethod
+  , primeCostMethod
+  , depreciationSchedule
+  , declineInValue
+  ) where
+
+import Data.Proxy (Proxy(..))
+
+import Data.Time (Day, Year, diffDays, fromGregorian)
+
+import Data.Tax
+import Data.Tax.ATO.FY
+
+-- | Effective life in years
+type EffectiveLife = Rational
+
+data DepreciatingAsset a = DepreciatingAsset
+  Day             -- ^ Start date of decline in value
+  (Money a)       -- ^ Asset's cost
+  EffectiveLife   -- ^ Effective life, in years.
+  (DepreciationMethod a)
+
+-- | Given the start date, effective life and the pair
+-- @(cost, baseValue)@, calculate the decline in value over
+-- a full year (i.e. /days held/ = 365).
+--
+type DepreciationMethod a =
+  Day -> EffectiveLife -> (Money a, Money a) -> Money a
+
+-- | Decline in value = @remainingValue * r / effectiveLife@,
+-- where @r = 1.5@ for assets acquired before 2006-05-10,
+-- otherwise @2.0@.
+diminishingValueMethod :: (Fractional a) => DepreciationMethod a
+diminishingValueMethod start life = ($/ fromRational life) . ($* factor) . snd
+  where
+  factor  | start < fromGregorian 2006 05 10  = 1.5
+          | otherwise                         = 2
+
+-- | /Straight-line depreciation/.
+-- Decline in value = cost / effective life.
+primeCostMethod :: (Fractional a) => DepreciationMethod a
+primeCostMethod _ life = ($/ fromRational life) . fst
+
+-- | Calculate decline in value over financial years
+depreciationSchedule
+  :: (Fractional a, Ord a)
+  => DepreciatingAsset a -> [(Year, Money a)]
+depreciationSchedule (DepreciatingAsset t1 cost life method) =
+  let
+    fy = financialYear t1
+    (_, eofy) = financialYearRange fy
+    daysList = diffDays eofy t1 + 1 : fmap daysInYear [fy + 1..]
+
+    step (_, value) nDays =
+      let
+        decline = (fromIntegral nDays *$ method t1 life (cost, value)) $/ 365
+      in
+        if decline > value
+          then (value, Money 0)
+          else (decline, value $-$ decline)
+  in
+    zip [fy..]
+    . takeWhile (>= Money 1)
+    . fmap fst
+    . drop 1
+    $ scanl step (Money 0, cost) daysList
+
+-- | Calculate decline in value for the given financial year
+-- (financial year given as type, via 'Proxy').
+declineInValue
+  :: (FinancialYear y, Fractional a, Ord a)
+  => Proxy y -> DepreciatingAsset a -> Money a
+declineInValue proxy asset =
+  case dropWhile ((< fromProxy proxy) . fst) (depreciationSchedule asset) of
+    (y,r):_ | y == fromProxy proxy  -> r
+    _                               -> Money 0
diff --git a/src/Data/Tax/ATO/FY.hs b/src/Data/Tax/ATO/FY.hs
--- a/src/Data/Tax/ATO/FY.hs
+++ b/src/Data/Tax/ATO/FY.hs
@@ -20,10 +20,7 @@
 
 -}
 
-{-# LANGUAGE ConstraintKinds #-}
 {-# LANGUAGE DataKinds #-}
-{-# LANGUAGE KindSignatures #-}
-{-# LANGUAGE ScopedTypeVariables #-}
 
 module Data.Tax.ATO.FY
   ( Days
@@ -33,9 +30,12 @@
   , getDays
   , getFraction
   , FinancialYear
+  , fromProxy
   , financialYear
   , financialYearRange
   , financialYearRangeFromProxy
+  , daysInYear
+  , daysInYearFromProxy
   )
   where
 
@@ -47,11 +47,19 @@
 
 type FinancialYear = KnownNat
 
-daysInYear :: KnownNat n => Proxy n -> Integer
-daysInYear proxy
-  | isLeapYear (natVal proxy) = 366
-  | otherwise                 = 365
+fromProxy :: (FinancialYear y) => Proxy y -> Year
+fromProxy = natVal
 
+-- | Number of days in the financial year ending June 30 of the given year.
+daysInYear :: Year -> Integer
+daysInYear y
+  | isLeapYear y  = 366
+  | otherwise     = 365
+
+-- | Number of days in the financial year ending June 30 (type-level variant).
+daysInYearFromProxy :: FinancialYear y => Proxy y -> Integer
+daysInYearFromProxy = daysInYear . natVal
+
 -- | Some number of days in a year.  Use 'days' to construct.
 newtype Days (n :: Nat) = Days
   { getDays :: Integer
@@ -62,11 +70,11 @@
 -- | Construct a 'Days' value.  If out of range, the number of days
 -- is clamped to 0 or 365/366 (no runtime errors).
 days :: forall a. (FinancialYear a) => Integer -> Days a
-days = Days . max 0 . min (daysInYear (Proxy :: Proxy a))
+days = Days . max 0 . min (daysInYearFromProxy (Proxy :: Proxy a))
 
 -- | Every day of the year
 daysAll :: forall a. (FinancialYear a) => Days a
-daysAll = Days (daysInYear (Proxy :: Proxy a))
+daysAll = Days (daysInYearFromProxy (Proxy :: Proxy a))
 
 -- | Zero days of the year
 daysNone :: Days a
@@ -76,7 +84,7 @@
 -- The denominator is determined by the year type.
 --
 getFraction :: forall a frac. (FinancialYear a, Fractional frac) => Days a -> frac
-getFraction n = fromRational $ getDays n % daysInYear (Proxy :: Proxy a)
+getFraction n = fromRational $ getDays n % daysInYearFromProxy (Proxy :: Proxy a)
 
 -- | The financial year in which the given day falls.
 --
diff --git a/src/Data/Tax/ATO/FY/FY2017.hs b/src/Data/Tax/ATO/FY/FY2017.hs
--- a/src/Data/Tax/ATO/FY/FY2017.hs
+++ b/src/Data/Tax/ATO/FY/FY2017.hs
@@ -17,13 +17,18 @@
 {-# LANGUAGE DataKinds #-}
 
 -- | Tax tables for 2016–17 financial year.
-module Data.Tax.ATO.FY.FY2017 (tables, individualIncomeTax) where
+module Data.Tax.ATO.FY.FY2017 (FY, fyProxy, tables, individualIncomeTax) where
 
-import Control.Lens (review)
+import Data.Proxy
+
 import Data.Tax
 import Data.Tax.ATO.Common
 import Data.Tax.ATO.PrivateHealthInsuranceRebate
 
+type FY = 2017
+fyProxy :: Proxy FY
+fyProxy = Proxy
+
 -- | In FY2017 the 37% threshold was raised from $80,000 to $87,000
 --
 individualIncomeTax :: (Fractional a, Ord a) => Tax (Money a) (Money a)
@@ -34,7 +39,7 @@
   , (180000, 0.45 - 0.37) ]
 
 temporaryBudgetRepairLevy :: (Fractional a, Ord a) => Tax (Money a) (Money a)
-temporaryBudgetRepairLevy = above (review money 180000) 0.02
+temporaryBudgetRepairLevy = above (Money 180000) 0.02
 
 help, sfss :: (Fractional a, Ord a) => Tax (Money a) (Money a)
 help = thresholds'
@@ -43,6 +48,16 @@
   , (82551, 0.005), (86895, 0.005), (95627, 0.005), (101900, 0.005) ]
 sfss = thresholds' [(54869, 0.02), (67369, 0.01), (95627, 0.01)]
 
+medicare :: (Fractional a) => MedicareLevyRatesAndThresholds a
+medicare = MedicareLevyRatesAndThresholds
+  { medicareLevyRate                                  = 0.02
+  , medicareLevyThresholdIndividual                   = Money 21655
+  , medicareLevyThresholdIndividualSeniorAndPensioner = Money 34244
+  , medicareLevyThresholdFamily                       = Money 36541
+  , medicareLevyThresholdFamilySeniorAndPensioner     = Money 47670
+  , medicareLevyThresholdDependentChildIncrease       = Money  3356
+  }
+
 -- | /Medicare levy surcharge (MLS)/.  Certain exemptions are available.
 --
 -- __Known issues__: the MLS is levied on taxable income + fringe
@@ -64,7 +79,7 @@
 tables :: (Ord a, Fractional a) => TaxTables 2017 a
 tables = TaxTables
   (individualIncomeTax <> temporaryBudgetRepairLevy)
-  (medicareLevy (review money 21656))
+  medicare
   medicareLevySurcharge
   help
   sfss
diff --git a/src/Data/Tax/ATO/FY/FY2018.hs b/src/Data/Tax/ATO/FY/FY2018.hs
--- a/src/Data/Tax/ATO/FY/FY2018.hs
+++ b/src/Data/Tax/ATO/FY/FY2018.hs
@@ -20,7 +20,6 @@
 module Data.Tax.ATO.FY.FY2018 (FY, fyProxy, tables) where
 
 import Data.Proxy
-import Control.Lens (review)
 import Data.Tax
 import Data.Tax.ATO.Common
 import Data.Tax.ATO.PrivateHealthInsuranceRebate
@@ -37,6 +36,16 @@
   , (84063, 0.005), (88487, 0.005), (97378, 0.005), (103766, 0.005) ]
 sfss = thresholds' [(55874, 0.02), (68603, 0.01), (97378, 0.01)]
 
+medicare :: (Fractional a) => MedicareLevyRatesAndThresholds a
+medicare = MedicareLevyRatesAndThresholds
+  { medicareLevyRate                                  = 0.02
+  , medicareLevyThresholdIndividual                   = Money 21980
+  , medicareLevyThresholdIndividualSeniorAndPensioner = Money 34758
+  , medicareLevyThresholdFamily                       = Money 37089
+  , medicareLevyThresholdFamilySeniorAndPensioner     = Money 48385
+  , medicareLevyThresholdDependentChildIncrease       = Money  3406
+  }
+
 -- | Individual tax rates unchanged from 2017.
 --
 -- The /temporary budget repair levy/ no longer applies.
@@ -44,7 +53,7 @@
 tables :: (Ord a, Fractional a) => TaxTables FY a
 tables = TaxTables
   FY2017.individualIncomeTax
-  (medicareLevy (review money 21980))
+  medicare
   (ttMedicareLevySurcharge FY2017.tables)
   help
   sfss
diff --git a/src/Data/Tax/ATO/FY/FY2019.hs b/src/Data/Tax/ATO/FY/FY2019.hs
--- a/src/Data/Tax/ATO/FY/FY2019.hs
+++ b/src/Data/Tax/ATO/FY/FY2019.hs
@@ -20,7 +20,6 @@
 module Data.Tax.ATO.FY.FY2019 (FY, fyProxy, tables) where
 
 import Data.Proxy
-import Control.Lens (review)
 
 import Data.Tax
 import Data.Tax.ATO.Common
@@ -46,6 +45,16 @@
   , (86856, 0.005), (91426, 0.005), (100614, 0.005), (107214, 0.005) ]
 sfss = thresholds' [(51957, 0.02), (64307, 0.01), (91426, 0.01)]
 
+medicare :: (Fractional a) => MedicareLevyRatesAndThresholds a
+medicare = MedicareLevyRatesAndThresholds
+  { medicareLevyRate                                  = 0.02
+  , medicareLevyThresholdIndividual                   = Money 22398
+  , medicareLevyThresholdIndividualSeniorAndPensioner = Money 35418
+  , medicareLevyThresholdFamily                       = Money 37794
+  , medicareLevyThresholdFamilySeniorAndPensioner     = Money 49304
+  , medicareLevyThresholdDependentChildIncrease       = Money  3471
+  }
+
 -- | The 37% threshold was increased from $87,000 to $90,000.
 --
 -- The new /low and middle income tax offset (LAMITO)/ was
@@ -54,7 +63,7 @@
 tables :: (Ord a, Fractional a) => TaxTables FY a
 tables = TaxTables
   individualIncomeTax
-  (medicareLevy (review money 22398))
+  medicare
   (ttMedicareLevySurcharge FY2018.tables)
   help
   sfss
diff --git a/src/Data/Tax/ATO/FY/FY2020.hs b/src/Data/Tax/ATO/FY/FY2020.hs
--- a/src/Data/Tax/ATO/FY/FY2020.hs
+++ b/src/Data/Tax/ATO/FY/FY2020.hs
@@ -20,7 +20,6 @@
 module Data.Tax.ATO.FY.FY2020 (FY, fyProxy, tables) where
 
 import Data.Proxy
-import Control.Lens (review)
 
 import Data.Tax
 import Data.Tax.ATO.Common
@@ -53,6 +52,16 @@
   , (134573, 0.005)
   ]
 
+medicare :: (Fractional a) => MedicareLevyRatesAndThresholds a
+medicare = MedicareLevyRatesAndThresholds
+  { medicareLevyRate                                  = 0.02
+  , medicareLevyThresholdIndividual                   = Money 22801
+  , medicareLevyThresholdIndividualSeniorAndPensioner = Money 36056
+  , medicareLevyThresholdFamily                       = Money 38474
+  , medicareLevyThresholdFamilySeniorAndPensioner     = Money 50191
+  , medicareLevyThresholdDependentChildIncrease       = Money  3533
+  }
+
 -- | From 1 July 2019, all study and training loans are covered by
 -- one set of thresholds and rates.  For backwards compatibility,
 -- 'ttHelp' and 'ttSfss' now refer to the same value.
@@ -60,7 +69,7 @@
 tables :: (Ord a, Fractional a) => TaxTables FY a
 tables = TaxTables
   (ttIndividualIncomeTax FY2019.tables)
-  (medicareLevy (review money 22801))
+  medicare
   (ttMedicareLevySurcharge FY2019.tables)
   help
   help
diff --git a/src/Data/Tax/ATO/FY/FY2021.hs b/src/Data/Tax/ATO/FY/FY2021.hs
--- a/src/Data/Tax/ATO/FY/FY2021.hs
+++ b/src/Data/Tax/ATO/FY/FY2021.hs
@@ -20,7 +20,6 @@
 module Data.Tax.ATO.FY.FY2021 (FY, fyProxy, tables, individualIncomeTax) where
 
 import Data.Proxy
-import Control.Lens (review)
 
 import Data.Tax
 import Data.Tax.ATO.Common
@@ -63,10 +62,20 @@
   , (136740, 0.005)
   ]
 
+medicare :: (Fractional a) => MedicareLevyRatesAndThresholds a
+medicare = MedicareLevyRatesAndThresholds
+  { medicareLevyRate                                  = 0.02
+  , medicareLevyThresholdIndividual                   = Money 23226
+  , medicareLevyThresholdIndividualSeniorAndPensioner = Money 36705
+  , medicareLevyThresholdFamily                       = Money 39167
+  , medicareLevyThresholdFamilySeniorAndPensioner     = Money 51094
+  , medicareLevyThresholdDependentChildIncrease       = Money  3597
+  }
+
 tables :: (Ord a, Fractional a) => TaxTables FY a
 tables = TaxTables
   individualIncomeTax
-  (medicareLevy (review money 23226))
+  medicare
   (ttMedicareLevySurcharge FY2020.tables)
   help
   help
diff --git a/src/Data/Tax/ATO/FY/FY2022.hs b/src/Data/Tax/ATO/FY/FY2022.hs
--- a/src/Data/Tax/ATO/FY/FY2022.hs
+++ b/src/Data/Tax/ATO/FY/FY2022.hs
@@ -20,7 +20,6 @@
 module Data.Tax.ATO.FY.FY2022 (FY, fyProxy, tables) where
 
 import Data.Proxy
-import Control.Lens (review)
 
 import Data.Tax
 import Data.Tax.ATO.Common
@@ -53,10 +52,20 @@
   , (137898, 0.005)
   ]
 
+medicare :: (Fractional a) => MedicareLevyRatesAndThresholds a
+medicare = MedicareLevyRatesAndThresholds
+  { medicareLevyRate                                  = 0.02
+  , medicareLevyThresholdIndividual                   = Money 23365
+  , medicareLevyThresholdIndividualSeniorAndPensioner = Money 36925
+  , medicareLevyThresholdFamily                       = Money 39402
+  , medicareLevyThresholdFamilySeniorAndPensioner     = Money 51401
+  , medicareLevyThresholdDependentChildIncrease       = Money  3619
+  }
+
 tables :: (Ord a, Fractional a) => TaxTables FY a
 tables = TaxTables
   FY2021.individualIncomeTax
-  (medicareLevy (review money 23365))
+  medicare
   (ttMedicareLevySurcharge FY2021.tables)
   help
   help
@@ -75,7 +84,7 @@
 -- | LMITO ceiling increased by $420 to $1500 in FY2022
 lmito2022 :: (Fractional a, Ord a) => Tax (Money a) (Money a)
 lmito2022 = limit mempty $
-  greaterOf (lump (review money (-1500)))
-    ( lump (review money (-255))
-    <> above (review money 37000) (-0.075) )
-  <> above (review money 90000) 0.03
+  greaterOf (lump (Money (-1500)))
+    ( lump (Money (-255))
+    <> above (Money 37000) (-0.075) )
+  <> above (Money 90000) 0.03
diff --git a/src/Data/Tax/ATO/FY/FY2023.hs b/src/Data/Tax/ATO/FY/FY2023.hs
--- a/src/Data/Tax/ATO/FY/FY2023.hs
+++ b/src/Data/Tax/ATO/FY/FY2023.hs
@@ -50,11 +50,20 @@
   , (141848, 0.005)
   ]
 
+medicare :: (Fractional a) => MedicareLevyRatesAndThresholds a
+medicare = MedicareLevyRatesAndThresholds
+  { medicareLevyRate                                  = 0.02
+  , medicareLevyThresholdIndividual                   = Money 24276
+  , medicareLevyThresholdIndividualSeniorAndPensioner = Money 38365
+  , medicareLevyThresholdFamily                       = Money 40939
+  , medicareLevyThresholdFamilySeniorAndPensioner     = Money 53406
+  , medicareLevyThresholdDependentChildIncrease       = Money  3760
+  }
+
 tables :: (Ord a, Fractional a) => TaxTables 2023 a
 tables = TaxTables
   (ttIndividualIncomeTax FY2022.tables)
-
-  (medicareLevy (Money 24276))
+  medicare
   (ttMedicareLevySurcharge FY2022.tables)
 
   help
diff --git a/src/Data/Tax/ATO/FY/FY2024.hs b/src/Data/Tax/ATO/FY/FY2024.hs
--- a/src/Data/Tax/ATO/FY/FY2024.hs
+++ b/src/Data/Tax/ATO/FY/FY2024.hs
@@ -22,6 +22,7 @@
 import Data.Proxy
 import Data.Tax
 import Data.Tax.ATO.Common
+import Data.Tax.ATO.PrivateHealthInsuranceRebate
 import qualified Data.Tax.ATO.FY.FY2023 as FY2023
 
 type FY = 2024
@@ -50,6 +51,16 @@
   , (151201, 0.005)
   ]
 
+medicare :: (Fractional a) => MedicareLevyRatesAndThresholds a
+medicare = MedicareLevyRatesAndThresholds
+  { medicareLevyRate                                  = 0.02
+  , medicareLevyThresholdIndividual                   = Money 26000
+  , medicareLevyThresholdIndividualSeniorAndPensioner = Money 41089
+  , medicareLevyThresholdFamily                       = Money 43846
+  , medicareLevyThresholdFamilySeniorAndPensioner     = Money 57198
+  , medicareLevyThresholdDependentChildIncrease       = Money  4027
+  }
+
 -- | Medicare levy surcharge thresholds changed for 2023–24
 medicareLevySurcharge :: (Fractional a, Ord a) => Tax (Money a) (Money a)
 medicareLevySurcharge =
@@ -57,18 +68,25 @@
   <> threshold (Money 108000) 0.0025
   <> threshold (Money 144000) 0.0025
 
+-- Rebate adjustment factor = 1.000 (no change)
+-- https://www.health.gov.au/news/phi-circulars/phi-1724-private-health-insurance-rebate-adjustment-factor-effective-1-april-2024
+-- However, the /thresholds/ did change this year.
+privateHealthInsuranceRebateRates
+  :: (Fractional a) => PrivateHealthInsuranceRebateRates a
+privateHealthInsuranceRebateRates =
+  [ ( 93000, (0.24608, 0.24608), (0.28710, 0.28710), (0.32812, 0.32812) )
+  , (108000, (0.16405, 0.16405), (0.20507, 0.20507), (0.24608, 0.24608) )
+  , (144000, (0.08202, 0.08202), (0.12303, 0.12303), (0.16405, 0.16405) )
+  ]
+
 tables :: (Ord a, Fractional a) => TaxTables FY a
 tables = TaxTables
   (ttIndividualIncomeTax FY2023.tables)
-
-  (medicareLevy (Money 26000))
-
+  medicare
   medicareLevySurcharge
   help
   help
 
   lowIncomeTaxOffset2021
 
-  -- Rebate adjustment factor = 1.000 (no change)
-  -- https://www.health.gov.au/news/phi-circulars/phi-1724-private-health-insurance-rebate-adjustment-factor-effective-1-april-2024
-  (ttPHIRebateRates FY2023.tables)
+  privateHealthInsuranceRebateRates
diff --git a/src/Data/Tax/ATO/FY/FY2025.hs b/src/Data/Tax/ATO/FY/FY2025.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Tax/ATO/FY/FY2025.hs
@@ -0,0 +1,102 @@
+-- This file is part of hs-tax-ato
+-- Copyright (C) 2024  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 DataKinds #-}
+
+-- | Tax tables for 2024–25 financial year.
+--
+-- These data are NOT YET FINAL.  We are waiting on federal budget
+-- (25 March 2025) to confirm Medicare Levy thresholds, and the PHI
+-- rebate adjustment factor (~1 April 2025).
+module Data.Tax.ATO.FY.FY2025 (FY, fyProxy, tables) where
+
+import Data.Proxy
+import Data.Tax
+import Data.Tax.ATO.Common
+import Data.Tax.ATO.PrivateHealthInsuranceRebate
+
+type FY = 2025
+fyProxy :: Proxy FY
+fyProxy = Proxy
+
+help :: (Fractional a, Ord a) => Tax (Money a) (Money a)
+help = thresholds'
+  [ (54435, 0.01)
+  , (62851, 0.01)
+  , (66621, 0.005)
+  , (70619, 0.005)
+  , (74856, 0.005)
+  , (79347, 0.005)
+  , (84108, 0.005)
+  , (89155, 0.005)
+  , (94504, 0.005)
+  , (100175, 0.005)
+  , (106186, 0.005)
+  , (122557, 0.005)
+  , (119310, 0.005)
+  , (126468, 0.005)
+  , (134057, 0.005)
+  , (142101, 0.005)
+  , (150627, 0.005)
+  , (159664, 0.005)
+  ]
+
+medicare :: (Fractional a) => MedicareLevyRatesAndThresholds a
+medicare = MedicareLevyRatesAndThresholds
+  { medicareLevyRate                                  = 0.02
+  , medicareLevyThresholdIndividual                   = Money 27222
+  , medicareLevyThresholdIndividualSeniorAndPensioner = Money 43020
+  , medicareLevyThresholdFamily                       = Money 45907
+  , medicareLevyThresholdFamilySeniorAndPensioner     = Money 59886
+  , medicareLevyThresholdDependentChildIncrease       = Money  4216
+  }
+
+-- | Medicare levy surcharge thresholds changed for 2024–25
+medicareLevySurcharge :: (Fractional a, Ord a) => Tax (Money a) (Money a)
+medicareLevySurcharge =
+  threshold (Money 97000) 0.01
+  <> threshold (Money 113000) 0.0025
+  <> threshold (Money 151000) 0.0025
+
+-- | In 2024–25 the "stage three tax cuts" came into effect.
+individualIncomeTax :: (Fractional a, Ord a) => Tax (Money a) (Money a)
+individualIncomeTax = marginal'
+  [ (18200, 0.16)
+  , (45000, 0.30 - 0.16)
+  , (135000, 0.37 - 0.30)
+  , (190000, 0.45 - 0.37) ]
+
+-- Rebate adjustment factor = TBC (currently re-using FY2024 rates)
+-- However, the /thresholds/ did change this year.
+privateHealthInsuranceRebateRates
+  :: (Fractional a) => PrivateHealthInsuranceRebateRates a
+privateHealthInsuranceRebateRates =
+  [ ( 97000, (0.24608, 0.24288), (0.28710, 0.28337), (0.32812, 0.32385) )
+  , (113000, (0.16405, 0.16192), (0.20507, 0.20240), (0.24608, 0.24288) )
+  , (151000, (0.08202, 0.08095), (0.12303, 0.12143), (0.16405, 0.16192) )
+  ]
+
+tables :: (Ord a, Fractional a) => TaxTables FY a
+tables = TaxTables
+  individualIncomeTax
+  medicare
+  medicareLevySurcharge
+  help
+  help
+
+  lowIncomeTaxOffset2021
+
+  privateHealthInsuranceRebateRates
diff --git a/src/Data/Tax/ATO/PaymentSummary.hs b/src/Data/Tax/ATO/PaymentSummary.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Tax/ATO/PaymentSummary.hs
@@ -0,0 +1,646 @@
+-- This file is part of hs-tax-ato
+-- Copyright (C) 2025  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 PatternSynonyms #-}
+
+{-|
+
+Types and optics for payment summaries.
+
+-}
+module Data.Tax.ATO.PaymentSummary
+  (
+  -- ** Individual non-business
+    PaymentSummaryIndividualNonBusiness
+  , newPaymentSummaryIndividualNonBusiness
+  , GrossPaymentsTypeIndividualNonBusiness(..)
+  , grossPaymentsTypeP
+  , grossPaymentsTypeH
+
+  -- *** Allowances
+  , allowances
+  , Allowance
+  , allowance
+  , allowanceDetail
+  , allowanceAmount
+
+  -- *** Fringe benefits
+  , HasReportableFringeBenefits(..)
+  , fringeBenefitsEmployerNotExempt
+  , fringeBenefitsEmployerExempt
+  , ReportableFringeBenefits(..)
+  , FBTEmployerExemption(..)
+
+  -- *** Lump sum A
+  , HasLumpSumA(..)
+  , lumpSumATypeR
+  , lumpSumATypeT
+  , LumpSumA
+  , lumpSumAType
+  , lumpSumAAmount
+  , LumpSumAType(..)
+
+  -- *** Lump sum B
+  , lumpSumB
+
+  -- *** Lump sum D
+  , HasLumpSumD(..)
+
+  -- *** Lump sum E
+  , HasLumpSumE(..)
+
+  -- *** Exempt foreign employment income
+  , exemptForeignEmploymentIncome
+
+  -- ** Foreign employment
+  , PaymentSummaryForeignEmployment
+  , newPaymentSummaryForeignEmployment
+  , GrossPaymentsTypeForeignEmployment(..)
+  , foreignTaxPaid
+
+  -- ** Business and personal services income
+  , PaymentSummaryBusinessAndPersonalServicesIncome
+  , newPaymentSummaryBusinessAndPersonalServicesIncome
+
+  -- ** Withholding where ABN not quoted
+  , PaymentSummaryWithholdingWhereABNNotQuoted
+  , newPaymentSummaryWithholdingWhereABNNotQuoted
+
+  -- ** Payer details
+  , PayerDetails
+  , newPayerDetails
+  , payerABN
+  , payerBranchNumber
+  , payerName
+  , HasPayerDetails(..)
+
+  -- ** Classes and helpers
+  , HasGrossPayments(..)
+  , HasGrossPaymentsType(..)
+  , HasTotalTaxWithheld(..)
+  , HasReportableEmployerSuperannuationContributions(..)
+  , pattern PaymentSummary
+  )
+  where
+
+import Data.String (IsString(..))
+
+import Control.Lens
+
+import Data.Tax
+import Data.Tax.ATO.ABN (ABN)
+import Data.Tax.ATO.Common
+import Data.Tax.ATO.Rounding
+
+
+-- | Deprecated pattern provided for compatibility.  Will be removed
+-- in a future release.  Use 'PaymentSummaryIndividualNonBusiness'
+-- constructor and accessors instead.
+--
+pattern PaymentSummary
+  :: (Num a) => PayerDetails -> Money a -> Money a -> Money a -> PaymentSummaryIndividualNonBusiness a
+pattern PaymentSummary payer gross tax resc <-
+  PaymentSummaryIndividualNonBusiness payer tax gross _type resc _fb
+    _allow _lumpA _lumpB _lumpD _lumpE _foreign
+  where
+  PaymentSummary payer gross tax resc = newPaymentSummaryIndividualNonBusiness payer
+    & set grossPayments gross
+    & set totalTaxWithheld tax
+    & set reportableEmployerSuperannuationContributions resc
+{-# DEPRECATED PaymentSummary "see instead PaymentSummaryIndividualNonBusiness" #-}
+
+
+-- | PAYG payment summary - individual non-business.
+--
+-- Use 'newPaymentSummaryIndividualNonBusiness' to construct this data
+-- type (all amounts initially zero).
+--
+-- To @set@ and @view@ the various fields, these lenses are available:
+--
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'payerDetails'                                  | Use 'newPayerDetails' to construct.     |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'totalTaxWithheld'                              | TOTAL TAX WITHHELD                      |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'grossPayments'                                 | GROSS PAYMENTS.  Do not include amounts |
+-- |                                                 | shown at 'allowances', 'lumpSumA',      |
+-- |                                                 | 'lumpSumB', 'lumpSumD', 'lumpSumE',     |
+-- |                                                 | or 'exemptForeignEmploymentIncome'.     |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'grossPaymentsType'                             | Use 'grossPaymentsTypeP' for non super  |
+-- |                                                 | pensions and annuities, or              |
+-- |                                                 | 'grossPaymentsTypeH' for working        |
+-- |                                                 | holiday makers. Otherwise, leave unset. |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'reportableEmployerSuperannuationContributions' | Reportable employer superannuation      |
+-- |                                                 | contributions (do not include           |
+-- |                                                 | compulsory super guarantee amounts)     |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'reportableFringeBenefits'                      | Fringe benefits amount for FBT year     |
+-- |                                                 | 1 April to 31 March, and employer       |
+-- |                                                 | FBT exemption status.  See also         |
+-- |                                                 | 'ReportableFringeBenefits'.             |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'exemptForeignEmploymentIncome'                 |                                         |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'allowances'                                    | List of 'allowance'.                    |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'lumpSumA'                                      | Lump sum A. Use 'lumpSumATypeR' or      |
+-- |                                                 | 'lumpSumATypeT' to construct.           |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'lumpSumB'                                      | Lump sum B.                             |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'lumpSumD'                                      | Lump sum D.                             |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'lumpSumE'                                      | Lump sum E - payments in arrears.  This |
+-- |                                                 | amount is included in taxable income.   |
+-- |                                                 | The tax offset calculations are not yet |
+-- |                                                 | implemented.                            |
+-- +-------------------------------------------------+-----------------------------------------+
+--
+data PaymentSummaryIndividualNonBusiness a = PaymentSummaryIndividualNonBusiness
+  { _inbPayer :: PayerDetails
+  , _inbWithholding :: Money a
+  , _inbGross :: Money a
+  , _inbGrossPaymentsType :: Maybe GrossPaymentsTypeIndividualNonBusiness
+  , _inbRESC :: Money a
+  , _inbFringeBenefits :: Maybe (ReportableFringeBenefits a)
+  , _inbAllowances :: [Allowance a]
+  , _inbLumpSumA :: Maybe (LumpSumA a)
+  , _inbLumpSumB :: Money a
+  , _inbLumpSumD :: Money a
+  , _inbLumpSumE :: Money a
+  , _inbForeign :: Money a
+  }
+
+-- | Construct a new payment summary.  All amounts are initially zero.
+newPaymentSummaryIndividualNonBusiness
+  :: (Num a) => PayerDetails -> PaymentSummaryIndividualNonBusiness a
+newPaymentSummaryIndividualNonBusiness payer =
+  PaymentSummaryIndividualNonBusiness payer
+    mempty    -- total tax withheld
+    mempty    -- gross payments
+    Nothing   -- gross payments type
+    mempty    -- reportable super contributions
+    Nothing   -- reportable fringe benefits
+    []        -- allowances
+    Nothing   -- lump sum a
+    mempty    -- lump sum b
+    mempty    -- lump sum d
+    mempty    -- lump sum e
+    mempty    -- exempt foreign employment income
+
+data GrossPaymentsTypeIndividualNonBusiness
+  = GrossPaymentsTypeP
+  -- ^ non super pensions or annuity.  See also 'grossPaymentsTypeP'.
+  | GrossPaymentsTypeH
+  -- ^ working holiday makers.  See also 'grossPaymentsTypeH'.
+  deriving (Eq, Ord)
+
+-- | Helper constructor - __P - non super pensions or annuity__
+grossPaymentsTypeP :: Maybe GrossPaymentsTypeIndividualNonBusiness
+grossPaymentsTypeP = Just GrossPaymentsTypeP
+
+-- | Helper constructor - __H - working holiday makers__
+grossPaymentsTypeH :: Maybe GrossPaymentsTypeIndividualNonBusiness
+grossPaymentsTypeH = Just GrossPaymentsTypeH
+
+instance (RealFrac a) => HasTaxableIncome PaymentSummaryIndividualNonBusiness a a where
+  taxableIncome = to $ \s ->
+    view grossPayments s
+    <> foldOf (allowances . traverse . allowanceAmount) s
+    <> foldOf (lumpSumA . traverse . lumpSumAAmount) s
+    <> view (lumpSumB . to (wholeDollars . ($/ 20))) s
+    <> view lumpSumE s
+
+instance HasTaxWithheld PaymentSummaryIndividualNonBusiness a a where
+  taxWithheld = totalTaxWithheld
+
+instance HasPayerDetails PaymentSummaryIndividualNonBusiness where
+  payerDetails = lens _inbPayer (\s b -> s { _inbPayer = b })
+
+instance HasTotalTaxWithheld PaymentSummaryIndividualNonBusiness where
+  totalTaxWithheld = lens _inbWithholding (\s b -> s { _inbWithholding = b })
+
+instance HasGrossPayments PaymentSummaryIndividualNonBusiness where
+  grossPayments = lens _inbGross (\s b -> s { _inbGross = b })
+
+instance HasGrossPaymentsType PaymentSummaryIndividualNonBusiness
+    (Maybe GrossPaymentsTypeIndividualNonBusiness) where
+  grossPaymentsType = lens _inbGrossPaymentsType (\s b -> s { _inbGrossPaymentsType = b })
+
+instance HasReportableEmployerSuperannuationContributions PaymentSummaryIndividualNonBusiness where
+  reportableEmployerSuperannuationContributions =
+    lens _inbRESC (\s b -> s { _inbRESC = b })
+
+instance HasReportableFringeBenefits PaymentSummaryIndividualNonBusiness where
+  reportableFringeBenefits =
+    lens _inbFringeBenefits (\s b -> s { _inbFringeBenefits = b })
+
+instance HasLumpSumA PaymentSummaryIndividualNonBusiness where
+  lumpSumA = lens _inbLumpSumA (\s b -> s { _inbLumpSumA = b })
+
+lumpSumB :: Lens' (PaymentSummaryIndividualNonBusiness a) (Money a)
+lumpSumB = lens _inbLumpSumB (\s b -> s { _inbLumpSumB = b })
+
+instance HasLumpSumD PaymentSummaryIndividualNonBusiness where
+  lumpSumD = lens _inbLumpSumD (\s b -> s { _inbLumpSumD = b })
+
+instance HasLumpSumE PaymentSummaryIndividualNonBusiness where
+  lumpSumE = lens _inbLumpSumE (\s b -> s { _inbLumpSumE = b })
+
+exemptForeignEmploymentIncome :: Lens' (PaymentSummaryIndividualNonBusiness a) (Money a)
+exemptForeignEmploymentIncome = lens _inbForeign (\s b -> s { _inbForeign = b })
+
+allowances :: Lens' (PaymentSummaryIndividualNonBusiness a) [Allowance a]
+allowances = lens _inbAllowances (\s b -> s { _inbAllowances = b })
+
+
+-- | PAYG payment summary - foreign employment
+--
+-- Use 'newPaymentSummaryForeignEmployment' to construct this data
+-- type (all amounts initially zero).
+--
+-- To @set@ and @view@ the various fields, these lenses are available:
+--
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'payerDetails'                                  | Use 'newPayerDetails' to construct.     |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'totalTaxWithheld'                              | TOTAL TAX WITHHELD                      |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'grossPayments'                                 | GROSS PAYMENTS.  You __must__ include   |
+-- |                                                 | allowances.  Do not include 'lumpSumA'  |
+-- |                                                 | 'lumpSumD' or 'lumpSumE' amounts.       |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'grossPaymentsType'                             | Initial value 'GrossPaymentsTypeF'.     |
+-- |                                                 | @set@ to 'GrossPaymentsTypeJ' for income|
+-- |                                                 | earned from work conducted in the Joint |
+-- |                                                 | Petroleum Development Area (JPDA).      |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'foreignTaxPaid'                                |                                         |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'reportableEmployerSuperannuationContributions' | Reportable employer superannuation      |
+-- |                                                 | contributions (do not include           |
+-- |                                                 | compulsory super guarantee amounts)     |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'reportableFringeBenefits'                      | Fringe benefits amount for FBT year     |
+-- |                                                 | 1 April to 31 March, and employer       |
+-- |                                                 | FBT exemption status.  See also         |
+-- |                                                 | 'ReportableFringeBenefits'.             |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'lumpSumA'                                      | Lump sum A. Use 'lumpSumATypeR' or      |
+-- |                                                 | 'lumpSumATypeT' to construct.           |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'lumpSumD'                                      | Lump sum D.                             |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'lumpSumE'                                      | Lump sum E - payments in arrears.  This |
+-- |                                                 | amount is included in taxable income.   |
+-- |                                                 | The tax offset calculations are not yet |
+-- |                                                 | implemented.                            |
+-- +-------------------------------------------------+-----------------------------------------+
+--
+data PaymentSummaryForeignEmployment a = PaymentSummaryForeignEmployment
+  { _foreignPayer :: PayerDetails
+  , _foreignWithholding :: Money a
+  , _foreignGross :: Money a
+  , _foreignGrossPaymentsType :: GrossPaymentsTypeForeignEmployment
+  , _foreignForeignTaxPaid :: Money a
+  , _foreignRESC :: Money a
+  , _foreignFringeBenefits :: Maybe (ReportableFringeBenefits a)
+  , _foreignLumpSumA :: Maybe (LumpSumA a)
+  , _foreignLumpSumD :: Money a
+  , _foreignLumpSumE :: Money a
+  }
+
+-- | Construct a new payment summary.  All amounts are initially zero.
+newPaymentSummaryForeignEmployment
+  :: (Num a) => PayerDetails -> PaymentSummaryForeignEmployment a
+newPaymentSummaryForeignEmployment payer =
+  PaymentSummaryForeignEmployment payer
+    mempty    -- total tax withheld
+    mempty    -- gross payments
+    GrossPaymentsTypeF   -- gross payments type
+    mempty    -- foreign tax paid
+    mempty    -- reportable super contributions
+    Nothing   -- reportable fringe benefits
+    Nothing   -- lump sum a
+    mempty    -- lump sum d
+    mempty    -- lump sum e
+
+data GrossPaymentsTypeForeignEmployment
+  = GrossPaymentsTypeF
+  -- ^ foreign employment income
+  | GrossPaymentsTypeJ
+  -- ^ income earned from work conducted in the Joint Petroleum
+  -- Development Area (JPDA).
+  deriving (Eq, Ord)
+
+instance (RealFrac a) => HasTaxableIncome PaymentSummaryForeignEmployment a a where
+  taxableIncome = to $ \s ->
+    view grossPayments s
+    <> foldOf (lumpSumA . traverse . lumpSumAAmount) s
+    <> view lumpSumE s
+
+instance HasTaxWithheld PaymentSummaryForeignEmployment a a where
+  taxWithheld = totalTaxWithheld
+
+instance HasPayerDetails PaymentSummaryForeignEmployment where
+  payerDetails = lens _foreignPayer (\s b -> s { _foreignPayer = b })
+
+instance HasTotalTaxWithheld PaymentSummaryForeignEmployment where
+  totalTaxWithheld = lens _foreignWithholding (\s b -> s { _foreignWithholding = b })
+
+instance HasGrossPayments PaymentSummaryForeignEmployment where
+  grossPayments = lens _foreignGross (\s b -> s { _foreignGross = b })
+
+instance HasGrossPaymentsType PaymentSummaryForeignEmployment
+    GrossPaymentsTypeForeignEmployment where
+  grossPaymentsType = lens _foreignGrossPaymentsType (\s b -> s { _foreignGrossPaymentsType = b })
+
+foreignTaxPaid :: Lens' (PaymentSummaryForeignEmployment a) (Money a)
+foreignTaxPaid =
+  lens _foreignForeignTaxPaid (\s b -> s { _foreignForeignTaxPaid = b })
+
+instance HasReportableEmployerSuperannuationContributions PaymentSummaryForeignEmployment where
+  reportableEmployerSuperannuationContributions =
+    lens _foreignRESC (\s b -> s { _foreignRESC = b })
+
+instance HasReportableFringeBenefits PaymentSummaryForeignEmployment where
+  reportableFringeBenefits =
+    lens _foreignFringeBenefits (\s b -> s { _foreignFringeBenefits = b })
+
+instance HasLumpSumA PaymentSummaryForeignEmployment where
+  lumpSumA = lens _foreignLumpSumA (\s b -> s { _foreignLumpSumA = b })
+
+instance HasLumpSumD PaymentSummaryForeignEmployment where
+  lumpSumD = lens _foreignLumpSumD (\s b -> s { _foreignLumpSumD = b })
+
+instance HasLumpSumE PaymentSummaryForeignEmployment where
+  lumpSumE = lens _foreignLumpSumE (\s b -> s { _foreignLumpSumE = b })
+
+
+-- | PAYG payment summary - business and personal services income
+--
+-- Use 'newPaymentSummaryBusinessAndPersonalServicesIncome' to construct this data
+-- type (all amounts initially zero).
+--
+-- To @set@ and @view@ the various fields, these lenses are available:
+--
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'payerDetails'                                  | Use 'newPayerDetails' to construct.     |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'totalTaxWithheld'                              | TOTAL TAX WITHHELD                      |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'grossPayments'                                 | Gross payments or gross attributed      |
+-- |                                                 | income.                                 |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'reportableEmployerSuperannuationContributions' | Reportable employer superannuation      |
+-- |                                                 | contributions (do not include           |
+-- |                                                 | compulsory super guarantee amounts)     |
+-- +-------------------------------------------------+-----------------------------------------+
+--
+data PaymentSummaryBusinessAndPersonalServicesIncome a = PaymentSummaryBusinessAndPersonalServicesIncome
+  { _bpsiPayer :: PayerDetails
+  , _bpsiWithholding :: Money a
+  , _bpsiGross :: Money a
+  , _bpsiRESC :: Money a
+  }
+
+-- | Construct a new payment summary.  All amounts are initially zero.
+newPaymentSummaryBusinessAndPersonalServicesIncome
+  :: (Num a) => PayerDetails -> PaymentSummaryBusinessAndPersonalServicesIncome a
+newPaymentSummaryBusinessAndPersonalServicesIncome payer =
+  PaymentSummaryBusinessAndPersonalServicesIncome payer
+    mempty    -- total tax withheld
+    mempty    -- gross payments
+    mempty    -- reportable super contributions
+
+instance HasTaxableIncome PaymentSummaryBusinessAndPersonalServicesIncome a a where
+  taxableIncome = grossPayments
+
+instance HasTaxWithheld PaymentSummaryBusinessAndPersonalServicesIncome a a where
+  taxWithheld = totalTaxWithheld
+
+instance HasPayerDetails PaymentSummaryBusinessAndPersonalServicesIncome where
+  payerDetails = lens _bpsiPayer (\s b -> s { _bpsiPayer = b })
+
+instance HasTotalTaxWithheld PaymentSummaryBusinessAndPersonalServicesIncome where
+  totalTaxWithheld = lens _bpsiWithholding (\s b -> s { _bpsiWithholding = b })
+
+instance HasGrossPayments PaymentSummaryBusinessAndPersonalServicesIncome where
+  grossPayments = lens _bpsiGross (\s b -> s { _bpsiGross = b })
+
+instance HasReportableEmployerSuperannuationContributions PaymentSummaryBusinessAndPersonalServicesIncome where
+  reportableEmployerSuperannuationContributions =
+    lens _bpsiRESC (\s b -> s { _bpsiRESC = b })
+
+
+-- | PAYG payment summary - withholding where ABN not quoted (other
+-- than those covered by annual investment income reporting).
+--
+-- Use 'newPaymentSummaryWithholdingWhereABNNotQuoted' to construct this data
+-- type (all amounts initially zero).
+--
+-- To @set@ and @view@ the various fields, these lenses are available:
+--
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'payerDetails'                                  | Use 'newPayerDetails' to construct.     |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'totalTaxWithheld'                              | TOTAL AMOUNT WITHHELD                   |
+-- +-------------------------------------------------+-----------------------------------------+
+-- | 'grossPayments'                                 | Gross payment (including the market     |
+-- |                                                 | value of non-cash benefits).            |
+-- +-------------------------------------------------+-----------------------------------------+
+--
+data PaymentSummaryWithholdingWhereABNNotQuoted a = PaymentSummaryWithholdingWhereABNNotQuoted
+  { _noabnPayer :: PayerDetails
+  , _noabnWithholding :: Money a
+  , _noabnGross :: Money a
+  }
+
+-- | Construct a new payment summary.  All amounts are initially zero.
+newPaymentSummaryWithholdingWhereABNNotQuoted
+  :: (Num a) => PayerDetails -> PaymentSummaryWithholdingWhereABNNotQuoted a
+newPaymentSummaryWithholdingWhereABNNotQuoted payer =
+  PaymentSummaryWithholdingWhereABNNotQuoted payer
+    mempty    -- total tax withheld
+    mempty    -- gross payments
+
+instance HasTaxableIncome PaymentSummaryWithholdingWhereABNNotQuoted a a where
+  taxableIncome = grossPayments
+
+instance HasTaxWithheld PaymentSummaryWithholdingWhereABNNotQuoted a a where
+  taxWithheld = totalTaxWithheld
+
+instance HasPayerDetails PaymentSummaryWithholdingWhereABNNotQuoted where
+  payerDetails = lens _noabnPayer (\s b -> s { _noabnPayer = b })
+
+instance HasTotalTaxWithheld PaymentSummaryWithholdingWhereABNNotQuoted where
+  totalTaxWithheld = lens _noabnWithholding (\s b -> s { _noabnWithholding = b })
+
+instance HasGrossPayments PaymentSummaryWithholdingWhereABNNotQuoted where
+  grossPayments = lens _noabnGross (\s b -> s { _noabnGross = b })
+
+
+-- | Objects which have a /GROSS PAYMENTS/ field.
+class HasGrossPayments s where
+  grossPayments :: Lens' (s a) (Money a)
+
+-- | Objects which have a /gross payments type/ field.
+-- The value type may vary.
+--
+class HasGrossPaymentsType s a where
+  grossPaymentsType :: Lens' (s any) a
+
+-- | Objects which have a /GROSS PAYMENTS/ field.
+class HasTotalTaxWithheld s where
+  totalTaxWithheld :: Lens' (s a) (Money a)
+
+-- | Objects which may have Reportable employer superannuation contributions
+class HasReportableEmployerSuperannuationContributions s where
+  reportableEmployerSuperannuationContributions :: Lens' (s a) (Money a)
+
+
+-- | Objects which may have reportable fringe benefit amounts
+class HasReportableFringeBenefits s where
+  reportableFringeBenefits :: Lens' (s a) (Maybe (ReportableFringeBenefits a))
+
+-- | Helper constructor for fringe benefits amount (employer not exempt from FBT).
+-- See also 'fringeBenefitsEmployerExempt'.
+fringeBenefitsEmployerNotExempt :: Money a -> Maybe (ReportableFringeBenefits a)
+fringeBenefitsEmployerNotExempt x = Just $ ReportableFringeBenefits x EmployerNotFBTExempt
+
+-- | Helper constructor for fringe benefits amount (employer exempt from FBT)
+-- See also 'fringeBenefitsEmployerNotExempt'.
+fringeBenefitsEmployerExempt :: Money a -> Maybe (ReportableFringeBenefits a)
+fringeBenefitsEmployerExempt x = Just $ ReportableFringeBenefits x EmployerFBTExempt
+
+data FBTEmployerExemption = EmployerNotFBTExempt | EmployerFBTExempt
+  deriving (Eq, Ord)
+
+-- | Reportable fringe benefits amount, with the employer exemption status.
+--
+-- For setting the fringe benefits amount in a
+-- 'PaymentSummaryIndividualNonBusiness', we provide more ergonomic helper
+-- constructors: 'fringeBenefitsEmployerNotExempt' and
+-- 'fringeBenefitsEmployerExempt'.
+--
+data ReportableFringeBenefits a =
+  ReportableFringeBenefits (Money a) FBTEmployerExemption
+  deriving (Eq, Ord)
+
+
+-- | Use 'allowance' to construct, and lenses 'allowanceDetail' and
+-- 'allowanceAmount' for field access.
+--
+data Allowance a = Allowance
+  { _allowanceDetail :: String
+  , _allowanceAmount :: (Money a)
+  }
+  deriving (Eq, Ord)
+
+allowance
+  :: (RealFrac a)
+  => String   -- ^ Allowance details
+  -> Money a  -- ^ Amount (discards cents)
+  -> Allowance a
+allowance s = Allowance s . wholeDollars
+
+allowanceDetail :: Lens' (Allowance a) String
+allowanceDetail = lens _allowanceDetail (\s b -> s { _allowanceDetail = b })
+
+allowanceAmount :: Lens (Allowance a) (Allowance b) (Money a) (Money b)
+allowanceAmount = lens _allowanceAmount (\s b -> s { _allowanceAmount = b })
+
+
+-- | Use 'lumpSumATypeR' or 'lumpSumATypeT' to construct, and lenses
+-- 'lumpSumAType' and 'lumpSumAAmount' for field access.
+data LumpSumA a = LumpSumA
+  { _lumpSumAType   :: LumpSumAType
+  , _lumpSumAAmount :: Money a
+  }
+  deriving (Eq, Ord)
+
+data LumpSumAType
+  = LumpSumATypeR
+  -- ^ if the payment was made for a genuine redundancy, invalidity
+  -- or under an early retirement scheme
+  | LumpSumATypeT
+  -- ^ if the payment was made for any other reason
+  deriving (Eq, Ord)
+
+-- | A /lump sum A/ payment made for genuine redundancy, invalidity
+-- or under an early retirement scheme.  See also 'lumpSumATypeT'.
+lumpSumATypeR :: (RealFrac a) => Money a -> LumpSumA a
+lumpSumATypeR = LumpSumA LumpSumATypeR . wholeDollars
+
+-- | A /lump sum A/ payment made for any other reason.
+-- See also 'lumpSumATypeR'.
+lumpSumATypeT :: (RealFrac a) => Money a -> LumpSumA a
+lumpSumATypeT = LumpSumA LumpSumATypeT . wholeDollars
+
+lumpSumAType :: Lens' (LumpSumA a) LumpSumAType
+lumpSumAType = lens _lumpSumAType (\s b -> s { _lumpSumAType = b })
+
+lumpSumAAmount :: Lens (LumpSumA a) (LumpSumA b) (Money a) (Money b)
+lumpSumAAmount = lens _lumpSumAAmount (\s b -> s { _lumpSumAAmount = b })
+
+class HasLumpSumA s where
+  lumpSumA :: Lens' (s a) (Maybe (LumpSumA a))
+
+
+class HasLumpSumD s where
+  lumpSumD :: Lens' (s a) (Money a)
+
+
+class HasLumpSumE s where
+  lumpSumE :: Lens' (s a) (Money a)
+
+
+-- | Payer details.  Use 'newPayerDetails' to construct.
+-- Use 'payerABN', 'payerBranchNumber' and 'payerName' lenses to view or
+-- set the fields.
+--
+-- Use the 'payerDetails' classy optic to access the payer details field
+-- in payment summary data types.
+--
+data PayerDetails = PayerDetails
+  { _payerABN :: ABN
+  , _payerBranch :: Maybe Int
+  , _payerName :: String
+  }
+  deriving (Eq, Ord)
+
+-- | __Deprecated__ instance that interprets string as ABN and
+-- sets payer name to the empty string.
+--
+instance IsString PayerDetails where
+  fromString s = PayerDetails (fromString s) Nothing ""
+
+newPayerDetails :: ABN -> Maybe Int -> String -> PayerDetails
+newPayerDetails = PayerDetails
+
+payerABN :: Lens' PayerDetails ABN
+payerABN = lens _payerABN (\s b -> s { _payerABN = b })
+
+payerBranchNumber :: Lens' PayerDetails (Maybe Int)
+payerBranchNumber = lens _payerBranch (\s b -> s { _payerBranch = b })
+
+payerName :: Lens' PayerDetails String
+payerName = lens _payerName (\s b -> s { _payerName = b })
+
+class HasPayerDetails s where
+  payerDetails :: Lens' (s a) PayerDetails
diff --git a/src/Data/Tax/ATO/Pretty.hs b/src/Data/Tax/ATO/Pretty.hs
--- a/src/Data/Tax/ATO/Pretty.hs
+++ b/src/Data/Tax/ATO/Pretty.hs
@@ -27,13 +27,12 @@
 module Data.Tax.ATO.Pretty
   ( summariseTaxReturnInfo
   , summariseAssessment
-  , summariseCGTAssessment
+  , formatMoney
   ) where
 
-import Data.Function (on)
-import Data.List (groupBy, sortOn)
+import Data.List.NonEmpty as NE (NonEmpty, groupAllWith, head)
 
-import Control.Lens (ALens', cloneLens, view, views)
+import Control.Lens (ALens', cloneLens, foldOf, view, views)
 import qualified Text.PrettyPrint as P
 
 import Data.Tax.ATO
@@ -62,6 +61,11 @@
 twoCol :: (P.Doc, Money Rational) -> P.Doc
 twoCol (label, value) = label P.$$ P.nest (80 - colWidthMoney) (formatMoney value)
 
+omitIfZero :: ((a, Money Rational) -> P.Doc) -> (a, Money Rational) -> P.Doc
+omitIfZero f rec@(_,x)
+  | x == mempty = P.empty
+  | otherwise   = f rec
+
 threeCol :: (P.Doc, Money Rational, Money Rational) -> P.Doc
 threeCol (label, v1, v2) =
   label
@@ -81,14 +85,18 @@
 summariseTaxReturnInfo info =
   "Income"
   P.$+$ vcatWith threeCol
-    [ ("  1   Salary or wages"  , view (paymentSummaries . taxWithheld) info, view (paymentSummaries . taxableIncome) info)
+    [ ( "  1   Salary or wages"
+      , view (paymentSummariesIndividualNonBusiness . taxWithheld) info
+      , view (paymentSummariesIndividualNonBusiness . taxableIncome) info
+      )
     , ("  10  Interest"         , view (interest . taxWithheld) info, view (interest . taxableIncome) info)
     ]
   P.$+$ "  11  Dividends"
   P.$+$ views dividends summariseDividends info
+  P.$+$ views ess summariseESS info
+  P.$+$ summariseCGT info
   P.$+$ vcatWith twoCol
-    [ ("  12  Employee share schemes" , view (ess . taxableIncome) info)
-    , ("  20M Other net foreign source income" , view foreignIncome info)
+    [ ("  20M Other net foreign source income" , view foreignIncome info)
     ]
   P.$+$ "Deductions"
   P.$+$ P.vcat (uncurry (summariseDeduction (view deductions info)) <$> deductionsTable)
@@ -100,16 +108,50 @@
 summariseDividends :: [Dividend Rational] -> P.Doc
 summariseDividends =
   vcatWith (threeCol . prep)
-  . groupBy ((==) `on` dividendSource)
-  . sortOn dividendSource
+  . groupAllWith dividendSource
   where
-    prep :: [Dividend Rational] -> (P.Doc, Money Rational, Money Rational)
+    prep :: NonEmpty (Dividend Rational) -> (P.Doc, Money Rational, Money Rational)
     prep l =
-      ( P.text ("        " <> dividendSource (head l))
+      ( P.text ("        " <> dividendSource (NE.head l))
       , view taxWithheld l
       , view taxableIncome l
       )
 
+summariseESS :: [ESSStatement Rational] -> P.Doc
+summariseESS [] = P.empty
+summariseESS l =
+  "  12  Employee share schemes"
+  P.$+$ P.vcat
+    [ twoCol
+        ( "    D Discount from upfront schemes - eligible for reduction"
+        , foldOf (traverse . essTaxedUpfrontReduction) l )
+    , twoCol
+        ( "    E Discount from upfront schemes - ineligible for reduction"
+        , foldOf (traverse . essTaxedUpfrontNoReduction) l )
+    , twoCol
+        ( "    F Discount from deferral schemes"
+        , foldOf (traverse . essDeferral) l )
+    , threeColLeft
+        ( "    C TFN amounts withheld from discounts"
+        , foldOf (traverse . essTFNAmounts) l )
+    , twoCol
+        ( "    A Foreign source discounts"
+        , foldOf (traverse . essForeignSourceDiscounts) l )
+    ]
+
+summariseCGT :: TaxReturnInfo y Rational -> P.Doc
+summariseCGT info
+  | o == nullCGTAssessment = P.empty
+  | otherwise =
+      "  18  Capital gains"
+      P.$+$ P.vcat
+        [ twoCol        ("    A Net capital gain",                   view cgtNetGain o)
+        , threeColLeft  ("    H Total current year capital gains",   view cgtTotalCurrentYearGains o)
+        , threeColLeft  ("    V Net capital losses carried forward", view cgtNetLossesCarriedForward o)
+        ]
+  where
+    o = assessCGTEvents (view capitalLossCarryForward info) (view cgtEvents info)
+
 deductionsTable :: [(ALens' (Deductions Rational) (Money Rational), String)]
 deductionsTable =
   [ (workRelatedCarExpenses, "D1  Work-related car expenses")
@@ -135,33 +177,34 @@
   -> String
   -> P.Doc
 summariseDeduction a l desc
-  | amt > mempty
-  = P.nest 2 (P.text desc) P.$$ P.nest colWidthLabel (formatMoney amt)
-  | otherwise
-  = P.empty
-  where
-    amt = view (cloneLens l) a
+  = omitIfZero twoCol (P.nest 2 (P.text desc), view (cloneLens l) a)
 
 
 summariseAssessment :: TaxAssessment Rational -> P.Doc
 summariseAssessment assessment =
   "Your taxable income is $" P.<> formatMoney (view taxableIncome assessment)
   P.$+$ P.text (replicate 80 '-')
-  P.$+$ vcatWith twoCol
-    [ ("Tax on your taxable income"                 , view taxDue assessment)
-    , ("Less credits and offsets"                   , views taxCreditsAndOffsets (fmap negate) assessment)
-    , ("Medicare levy (and surcharge, if any)"      , view medicareLevyDue assessment)
-    , ("Study and training loan repayment"          , view studyAndTrainingLoanRepayment assessment)
-    , ("Excess private health reduction or refund"  , view privateHealthInsuranceRebateAdjustment assessment)
-    , ("Less PAYG withholding"                      , views taxWithheld (fmap negate) assessment)
-    , ("Less PAYG instalments"                      , views paygInstalmentsCredit (fmap negate) assessment)
+  P.$+$ threeColLeft ("Tax on your taxable on net income"          , view taxDue assessment)
+  P.$+$ "Less non-refundable tax offsets"
+  P.$+$ vcatWith (omitIfZero twoCol)
+    [ ("  Offset for super contributions on behalf of your spouse"
+      , view offsetForSuperannuationContributionsOnBehalfOfYourSpouse assessment )
+    , ("  Foreign income tax offsets"                 , view foreignIncomeTaxOffsets assessment)
     ]
+  P.$+$ "Less refundable tax offsets"
+  P.$+$ twoCol ("  Franking credit offset"            , view frankingCreditOffset assessment)
+  P.$+$ "Plus other liabilities"
+  P.$+$ vcatWith (omitIfZero threeColLeft)
+    [ ("  Medicare levy"                              , view medicareLevyDue assessment)
+    , ("  Medicare levy surcharge"                    , view medicareLevySurchargeDue assessment)
+    , ("  Study and training loan repayment"          , view studyAndTrainingLoanRepayment assessment)
+    , ("  Excess private health reduction or refund"  , view privateHealthInsuranceRebateAdjustment assessment)
+    ]
+  P.$+$ "Less Pay as you go (PAYG) credits and other entitlements"
+  P.$+$ vcatWith (omitIfZero twoCol)
+    [ ("  PAYG instalments"                           , view paygInstalmentsCredit assessment)
+    , ("  PAYG withholding"                           , view taxWithheld assessment)
+    ]
   P.$+$ P.text (replicate 80 '-')
   P.$+$ "Result of this notice" P.$$ P.nest colWidthLabel (views taxBalance formatMoney assessment)
   P.$+$ "Net capital loss to carry forward" P.$$ P.nest colWidthLabel (views (taxCGTAssessment . capitalLossCarryForward) formatMoney assessment)
-
-summariseCGTAssessment :: CGTAssessment Rational -> P.Doc
-summariseCGTAssessment cgtAss@(CGTAssessment total _) =
-  "Total FY capital gains" P.$$ (P.nest colWidthLabel . formatMoney) total
-  P.$+$ "Net capital gain" P.$$ P.nest colWidthLabel (views cgtNetGain formatMoney cgtAss)
-  P.$+$ "Net capital loss to carry forward" P.$$ P.nest colWidthLabel (views capitalLossCarryForward formatMoney cgtAss)
diff --git a/src/Data/Tax/ATO/PrivateHealthInsuranceRebate.hs b/src/Data/Tax/ATO/PrivateHealthInsuranceRebate.hs
--- a/src/Data/Tax/ATO/PrivateHealthInsuranceRebate.hs
+++ b/src/Data/Tax/ATO/PrivateHealthInsuranceRebate.hs
@@ -14,8 +14,6 @@
 -- 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.
diff --git a/src/Data/Tax/ATO/TFN.hs b/src/Data/Tax/ATO/TFN.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Tax/ATO/TFN.hs
@@ -0,0 +1,94 @@
+-- This file is part of hs-tax-ato
+-- Copyright (C) 2025  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/>.
+
+{-|
+
+Tax File Number (TFN) types and functions.
+
+@
+import qualified Data.Tax.ATO.TFN as TFN
+
+main = do
+  let
+    tfnStr = "330 600 376"
+    Just tfn = TFN.parse tfnStr
+    tfnInt = TFN.asInt tfn
+    Just tfn' = TFN.fromInt tfnInt
+  putStrLn $ TFN.asString tfn'  -- prints "330 600 376"
+@
+
+It is recommended to import this module qualified.
+
+You can use the @OverloadedStrings@ language extension or the 'parse' function
+to convert string to TFNs.  Space characters are ignored, but other non-digits
+will cause parse failure.
+
+-}
+
+module Data.Tax.ATO.TFN
+  (
+    TFN
+  , parse
+  , fromInt
+  , asString
+  , asInt
+  ) where
+
+import Data.Char (isDigit)
+import Data.String
+import Data.Maybe (fromJust)
+
+-- | Tax File Number (TFN)
+newtype TFN = TFN Int
+  deriving (Eq, Ord, Show)
+
+-- | Parse a TFN, throwing an error if it is not valid.
+instance IsString TFN where
+  fromString = fromJust . parse
+
+-- | Construct a TFN, checking validity.
+fromInt :: Int -> Maybe TFN
+fromInt n | valid n = Just (TFN n)
+fromInt _           = Nothing
+
+-- | Parse a TFN.  Spaces characters are allowed, other non-digit
+-- characters are not.
+parse :: String -> Maybe TFN
+parse s
+  | all isDigit s'  = fromInt (read s')
+  | otherwise       = Nothing
+  where s' = filter (/= ' ') s
+
+valid :: Int -> Bool
+valid n =
+  (== 0) . (`mod` 11) . snd $ foldr step (n, 0) weights
+  where
+    weights = [1,4,3,7,5,8,6,9,10]
+    step weight (n',acc) = ((+ acc) . (* weight)) <$> divMod n' 10
+
+asInt :: TFN -> Int
+asInt (TFN n) = n
+
+-- Format TFN in the standard way (XXX XXX XXX)
+asString :: TFN -> String
+asString (TFN n) =
+  case s' of
+    [a1,a2,a3,b1,b2,b3,c1,c2,c3]  -> [a1,a2,a3,' ',b1,b2,b3,' ',c1,c2,c3]
+    _                             -> show n -- can't happen, but just in case...
+  where
+    n' = abs n  -- negative can't happen, but just in case...
+    s = show n'
+    s' = replicate (9 - length s) '0' <> s  -- left-pad to 9 digits
diff --git a/tax-ato.cabal b/tax-ato.cabal
--- a/tax-ato.cabal
+++ b/tax-ato.cabal
@@ -1,6 +1,6 @@
 cabal-version:       2.2
 name:                tax-ato
-version:             2024.1.0.1
+version:             2025.1
 synopsis:            Tax types and computations for Australia
 description:
   This library provides types and tax computations for tax
@@ -14,7 +14,7 @@
 copyright:           Copyright (C) 2018-2023 Fraser Tweedale
 category:            Finance
 build-type:          Simple
-tested-with:         GHC ==9.2.8 || ==9.4.7 || ==9.6.3 || ==9.8.1
+tested-with:         GHC ==9.6.7 || ==9.8.4 || ==9.10.2 || ==9.12.2
 extra-source-files:
   .hlint.yaml
 extra-doc-files:
@@ -27,7 +27,7 @@
   location: https://github.com/frasertweedale/hs-tax-ato.git
 
 common common
-  default-language: Haskell2010
+  default-language: GHC2021
   ghc-options:
     -Wall
     -Wcompat
@@ -41,18 +41,12 @@
     -Wredundant-constraints
     -Wunused-packages
     -fhide-source-paths
-  if impl(ghc >= 9.0)
-    ghc-options:
-      -Winvalid-haddock
-      -Werror=unicode-bidirectional-format-characters
-  if impl(ghc >= 9.2)
-    ghc-options:
-      -Wimplicit-lift
-      -Woperator-whitespace
-      -Wredundant-bang-patterns
-  if impl(ghc >= 9.4)
-    ghc-options:
-      -Wredundant-strictness-flags
+    -Winvalid-haddock
+    -Werror=unicode-bidirectional-format-characters
+    -Wimplicit-lift
+    -Woperator-whitespace
+    -Wredundant-bang-patterns
+    -Wredundant-strictness-flags
   build-depends:
     base >= 4.14 && < 5
 
@@ -61,8 +55,12 @@
   hs-source-dirs:      src
   exposed-modules:
     Data.Tax.ATO
+    Data.Tax.ATO.ABN
+    Data.Tax.ATO.TFN
     Data.Tax.ATO.CGT
     Data.Tax.ATO.Common
+    Data.Tax.ATO.Depreciation
+    Data.Tax.ATO.PaymentSummary
     Data.Tax.ATO.Pretty
     Data.Tax.ATO.PrivateHealthInsuranceRebate
     Data.Tax.ATO.Rounding
@@ -75,8 +73,9 @@
     Data.Tax.ATO.FY.FY2022
     Data.Tax.ATO.FY.FY2023
     Data.Tax.ATO.FY.FY2024
+    Data.Tax.ATO.FY.FY2025
   build-depends:
-    , lens >= 4.12 && < 6
-    , time >= 1.11 && < 1.13
+    , lens >= 4.18 && < 6
+    , time >= 1.11 && < 1.15
     , tax >= 0.2 && < 0.3
     , pretty >= 1.1.3.6 && < 2
