diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,43 @@
+## Version 2024.1
+
+- The minimum supported version of GHC is now 9.2, due to a
+  dependency on more recent versions of the *time* library.
+
+- Update the `Dividend` type to use store gross payment amount and
+  tax withheld, rather than net amount, franking portion and (other)
+  tax withheld.  Add new helper functions for construction:
+  - `dividendFromGross` takes gross amount and tax withheld
+  - `dividendFromNet` takes net amount and tax withheld
+  - `dividendFromNetFranked` takes net amount, franked proportion and
+    applicable corporate tax rate for working out the franking credit.
+  - `dividendFromNetFranked30` is a shortcut that uses the standard
+    corporate tax rate of 30%.
+
+- Change the type of `dividendDate` field from `String` to
+  `Data.Time.Day`.
+
+- Rename the `HasIncome` class to `HasTaxableIncome`, and its
+  member function `income` to `taxableIncome`.
+
+- Add functions for getting the `Day` range of a financial year,
+  and a function for getting the financial year of a given day.
+
+- Move the `Data.Tax.ATO.Days` module to `Data.Tax.ATO.FY` and
+  rename the `DaysInYear` type synonym to `FinancialYear`, to
+  reflect the additional behaviour.
+
+- Each `FY.<YYYY>` module now exports an `FY` type synonym for its
+  type-level `Nat` representing the financial year, and the `fyProxy
+  :: Proxy FY` value.
+
+- Add `Functor` instance for `CGTEvent`.
+
+- Add `FY2024` module.
+
+- Add `Data.Tax.ATO.Pretty` module, which provides pretty printers
+  for `TaxReturnInfo`, `TaxAssessment`, and other data types.
+
+
 ## Version 2023.2
 
 - Add support for PAYG Instalments, which are specified in aggregate
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
@@ -39,7 +39,7 @@
     TaxReturnInfo
   , newTaxReturnInfo
   , newTaxReturnInfoForTables
-  , income
+  , taxableIncome
 
   -- ** Income
 
@@ -51,10 +51,13 @@
   -- *** Interest
   , interest
 
-  -- *** Dividends and franking credits
+  -- *** Dividends
   , Dividend(..)
   , dividends
-  , dividendFrankingCredit
+  , dividendFromGross
+  , dividendFromNet
+  , dividendFromNetFranked
+  , dividendFromNetFranked30
 
   -- *** Capital gains tax (CGT)
   , HasCapitalLossCarryForward(..)
@@ -131,7 +134,9 @@
   , taxBalance
   , taxDue
   , medicareLevyDue
+  , studyAndTrainingLoanRepayment
   , taxCreditsAndOffsets
+  , paygInstalmentsCredit
   , taxCGTAssessment
   , privateHealthInsuranceRebateAdjustment
 
@@ -150,11 +155,12 @@
   ) where
 
 import Control.Lens (Getter, Lens', (&), foldOf, lens, set, to, view, views)
+import Data.Time (Day)
 
 import Data.Tax
 import Data.Tax.ATO.CGT
 import Data.Tax.ATO.Common
-import Data.Tax.ATO.Days
+import Data.Tax.ATO.FY
 import Data.Tax.ATO.PrivateHealthInsuranceRebate
 import Data.Tax.ATO.Rounding
 
@@ -217,13 +223,14 @@
 -- +---------------------------------------+----------------------------------+
 -- | 'mlsExemption'                        | Medicare levy exemption          |
 -- +---------------------------------------+----------------------------------+
--- | 'helpBalance'                         | HELP account balance             |
+-- | 'helpBalance'                         | HELP, VSL, SSL, ABSTUDY SSL,     |
+-- |                                       | and TSL account balance          |
 -- +---------------------------------------+----------------------------------+
 -- | 'sfssBalance'                         | SFSS account balance             |
 -- +---------------------------------------+----------------------------------+
 -- | 'paymentSummaries'                    | PAYG payment summaries           |
 -- +---------------------------------------+----------------------------------+
--- | 'interest'                            | Interest data                    |
+-- | 'interest'                            | Interest income and tax withheld |
 -- +---------------------------------------+----------------------------------+
 -- | 'dividends'                           | Dividend data                    |
 -- +---------------------------------------+----------------------------------+
@@ -272,7 +279,7 @@
 -- fully exempt).
 --
 newTaxReturnInfo
-  :: (DaysInYear y, Num a)
+  :: (FinancialYear y, Num a)
   => TaxReturnInfo y a
 newTaxReturnInfo = TaxReturnInfo
   daysAll  -- MLS exemption
@@ -296,7 +303,7 @@
 -- argument (which is ignored).
 --
 newTaxReturnInfoForTables
-  :: (DaysInYear y, Num a)
+  :: (FinancialYear y, Num a)
   => TaxTables y a -> TaxReturnInfo y a
 newTaxReturnInfoForTables _ = newTaxReturnInfo
 
@@ -304,9 +311,16 @@
   capitalLossCarryForward = lens _triCapitalLossCarryForward
       (\s b -> s { _triCapitalLossCarryForward = b })
 
+-- | HELP, VSL, SSL, ABSTUDY SSL, and TSL account balance.
 helpBalance :: Lens' (TaxReturnInfo y a) (Money a)
 helpBalance = lens _helpBalance (\s b -> s { _helpBalance = b })
 
+-- | SFSS account balance.  From 1 July 2019, all study and training
+-- loans are covered by one set of threshold and rates.  Since then,
+-- you can specify your entire study and training loan balance via
+-- 'helpBalance'.  But it will still calculate correctly if you specify
+-- your SFSS balance separately.
+--
 sfssBalance :: Lens' (TaxReturnInfo y a) (Money a)
 sfssBalance = lens _sfssBalance (\s b -> s { _sfssBalance = b })
 
@@ -358,11 +372,12 @@
   , _taxCreditsAndOffsets :: Money a
   , _taCGTAssessment :: CGTAssessment a
   , _phiAdj :: Money a
+  , _studyAndTrainingLoanRepayment :: Money a
+  , _paygInstalmentsCredit :: Money a
   }
 
--- | Taxable income
-instance HasIncome TaxAssessment a a where
-  income = to _taxableIncome
+instance HasTaxableIncome TaxAssessment a a where
+  taxableIncome = to _taxableIncome
 
 instance HasTaxWithheld TaxAssessment a a where
   taxWithheld = to _taxWithheld
@@ -379,9 +394,17 @@
 taxCGTAssessment :: Lens' (TaxAssessment a) (CGTAssessment a)
 taxCGTAssessment = lens _taCGTAssessment (\s b -> s { _taCGTAssessment = b })
 
+studyAndTrainingLoanRepayment :: Lens' (TaxAssessment a) (Money a)
+studyAndTrainingLoanRepayment =
+  lens _studyAndTrainingLoanRepayment (\s b -> s { _studyAndTrainingLoanRepayment = b })
+
 privateHealthInsuranceRebateAdjustment :: Lens' (TaxAssessment a) (Money a)
 privateHealthInsuranceRebateAdjustment = lens _phiAdj (\s b -> s { _phiAdj = b })
 
+paygInstalmentsCredit :: Lens' (TaxAssessment a) (Money a)
+paygInstalmentsCredit =
+  lens _paygInstalmentsCredit (\s b -> s { _paygInstalmentsCredit = b })
+
 -- | 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)
@@ -389,8 +412,10 @@
   view taxWithheld a
   $-$ view taxDue a
   $-$ view medicareLevyDue a
+  $-$ view studyAndTrainingLoanRepayment a
   $-$ view privateHealthInsuranceRebateAdjustment a
   $+$ view taxCreditsAndOffsets a
+  $+$ view paygInstalmentsCredit a
 
 instance (Num a, Eq a) => HasCapitalLossCarryForward TaxAssessment a where
   capitalLossCarryForward = taxCGTAssessment . capitalLossCarryForward
@@ -408,18 +433,18 @@
 
 -- | Tax to calculate compulsory study and training loan repayments
 -- (e.g. HELP, SFSS)
-studyAndTrainingLoanRepayment
+studyAndTrainingLoanRepaymentTax
   :: (Fractional a, Ord a)
   => TaxTables y a
   -> TaxReturnInfo y a
   -> Tax (Money a) (Money a)
-studyAndTrainingLoanRepayment table info =
+studyAndTrainingLoanRepaymentTax table info =
   limit (view helpBalance info) (ttHelp table)
   <> limit (view sfssBalance info) (ttSfss table)
 
 -- | Medicare levy + surcharge
 medicareLevyTax
-  :: (DaysInYear y, Fractional a)
+  :: (FinancialYear y, Fractional a)
   => TaxTables y a
   -> TaxReturnInfo y a
   -> Tax (Money a) (Money a)    -- grand unified individual income tax
@@ -436,21 +461,23 @@
     <> fmap ($* mlsFrac) mls
 
 -- | Taxable income
-instance (RealFrac a) => HasIncome (TaxReturnInfo y) a a where
-  income = to $ \info ->
+instance (RealFrac a) => HasTaxableIncome (TaxReturnInfo y) a a where
+  taxableIncome = to $ \info ->
     let
       cf = view capitalLossCarryForward info
       gross = foldMap wholeDollars
-        [ view (paymentSummaries . income) info
-        , view (interest . income) info
-        , view (dividends . income) info
-        , view (ess . income) info
+        [ view (paymentSummaries . taxableIncome) info
+        , view (interest . taxableIncome) info
+        , view (dividends . taxableIncome) info
+        , view (ess . taxableIncome) info
         , view (cgtEvents . to (assessCGTEvents cf) . cgtNetGain) info
         , view foreignIncome info
         ]
     in
       wholeDollars (gross $-$ views deductions totalDeductions info)
 
+-- | Includes PAYG withholding by employer or bank.
+-- Does not include franking credits.
 instance (Num a) => HasTaxWithheld (TaxReturnInfo y) a a where
   taxWithheld = to $ \info ->
     view (paymentSummaries . taxWithheld) info
@@ -458,15 +485,15 @@
 
 -- | Assess a tax return, given tax tables and tax return info.
 assessTax
-  :: (DaysInYear y, RealFrac a)
+  :: (FinancialYear y, RealFrac a)
   => TaxTables y a -> TaxReturnInfo y a -> TaxAssessment a
 assessTax tables info =
   let
     cg = assessCGTEvents
           (view capitalLossCarryForward info) (view cgtEvents info)
-    taxable = view income info
+    taxable = view taxableIncome info
     due = getTax (individualTax tables) taxable
-    studyRepayment = getTax (studyAndTrainingLoanRepayment tables info) taxable
+    studyRepayment = getTax (studyAndTrainingLoanRepaymentTax tables info) taxable
     mlAndMLS = getTax (medicareLevyTax tables info) taxable
 
     incomeForSurchargePurposes =
@@ -491,7 +518,9 @@
         step2 =
           let
             info' = info & set foreignIncome mempty
-            taxable' = view income info' <> view (deductions . foreignIncomeDeductions) info'
+            taxable' =
+              view taxableIncome info'
+              <> view (deductions . foreignIncomeDeductions) info'
             due' = getTax (individualTax tables) taxable'
             mlAndMLS' = getTax (medicareLevyTax tables info') taxable'
           in
@@ -500,23 +529,22 @@
       in
         max (Money 1000) step3
 
-    frankingCredit =
-      wholeDollars
-      $ foldMap dividendFrankingCredit (view dividends info)
+    frankingCredit = wholeDollars $ view (dividends . taxWithheld) info
     off =
       view (offsets . spouseContributionOffset) info
       <> min (view (offsets . foreignTaxOffset) info) foreignIncomeTaxOffsetLimit
-      <> view (offsets . paygInstalments) info
 
   in
     TaxAssessment
       taxable
-      (due <> studyRepayment)
+      due
       mlAndMLS
       (view taxWithheld info)
       (frankingCredit <> off)
       cg
       phiAdj
+      studyRepayment
+      (view (offsets . paygInstalments) info)
 
 -- | Australian Business Number
 type ABN = String
@@ -530,8 +558,8 @@
   }
 
 -- | Gross income
-instance HasIncome PaymentSummary a a where
-  income = to summaryGross
+instance HasTaxableIncome PaymentSummary a a where
+  taxableIncome = to summaryGross
 
 instance HasTaxWithheld PaymentSummary a a where
   taxWithheld = to summaryWithheld
@@ -552,30 +580,83 @@
 -- and amount of tax withheld.
 data Dividend a = Dividend
   { dividendSource :: String
-  , dividendDate :: String  -- FUTURE better type
-  , dividendNetPayment :: Money a
-  , dividendFrankedPortion :: Proportion a  -- ^ Franked ratio (@1@ = 100%)
-  , dividendTaxWithheld :: Money a
+  , dividendDate :: Day
+  , dividendGrossAndWithheld :: GrossAndWithheld a
   }
-  deriving (Show)
 
+-- | Rounds to whole cents
 instance (RealFrac a) => HasTaxWithheld Dividend a a where
-  taxWithheld = to (roundCents . dividendTaxWithheld)
+  taxWithheld = to dividendGrossAndWithheld . taxWithheld . to roundCents
 
--- | Calculate the franking credit for a dividend
+-- | Rounds to whole cents
+instance (RealFrac a) => HasTaxableIncome Dividend a a where
+  taxableIncome = to dividendGrossAndWithheld . taxableIncome . to roundCents
+
+-- | Construct a dividend from a net payment, with a proportion
+-- of the dividend franked at the given corporate tax rate (must
+-- be a flat rate).
 --
-dividendFrankingCredit :: (RealFrac a) => Dividend a -> Money a
-dividendFrankingCredit d = roundCents $
-  (getProportion . dividendFrankedPortion) d
-  *$ getTax corporateTax (dividendNetPayment d $* (1 / 0.7))
+-- Does not perform rounding.
+--
+dividendFromNetFranked
+  :: (Fractional a)
+  => String         -- ^ Source name (e.g. ticker)
+  -> Day            -- ^ Dividend date
+  -> Money a        -- ^ Net payment
+  -> Proportion a   -- ^ Franked proportion
+  -> Tax (Money a) (Money a)  -- ^ Corporate tax rate (must be a flat rate)
+  -> Dividend a
+dividendFromNetFranked src date net franked rate =
+  dividendFromGross src date gross withheld
+  where
+    Money r = getTax rate (Money 1)  -- extract flat tax rate
+    withheld = net $* ( getProportion franked * r / (1 - r) )
+    gross = net <> withheld
 
--- | Attributable income
-instance (RealFrac a) => HasIncome Dividend a a where
-  income = to $ \d ->
-    roundCents (dividendNetPayment d)
-    <> dividendFrankingCredit d
-    <> view taxWithheld d
+-- | Construct a dividend from a net payment, with a proportion
+-- of the dividend franked at the 30% corporate tax rate.
+--
+-- Does not perform rounding.
+--
+dividendFromNetFranked30
+  :: (Fractional a)
+  => String         -- ^ Source name (e.g. ticker)
+  -> Day            -- ^ Dividend date
+  -> Money a        -- ^ Net payment
+  -> Proportion a   -- ^ Franked proportion
+  -> Dividend a
+dividendFromNetFranked30 src date net franked =
+  dividendFromNetFranked src date net franked corporateTax
 
+-- | Construct a dividend from a net payment, with explicit
+-- declaration of tax withheld.
+--
+-- Does not perform rounding.
+--
+dividendFromNet
+  :: (Num a)
+  => String         -- ^ Source name (e.g. ticker)
+  -> Day            -- ^ Dividend date
+  -> Money a        -- ^ Net payment
+  -> Money a        -- ^ Tax withheld
+  -> Dividend a
+dividendFromNet src date net withheld =
+  dividendFromGross src date (net <> withheld) withheld
+
+-- | Construct a dividend from a gross payment, with explicit
+-- declaration of tax withheld.
+--
+-- Does not perform rounding.
+--
+dividendFromGross
+  :: String         -- ^ Source name (e.g. ticker)
+  -> Day            -- ^ Dividend date
+  -> Money a        -- ^ Gross payment
+  -> Money a        -- ^ Tax withheld
+  -> Dividend a
+dividendFromGross src date gross withheld =
+  Dividend src date (GrossAndWithheld gross withheld)
+
 -- | Tax offsets that individuals can claim
 --
 -- The following lenses are available:
@@ -615,10 +696,42 @@
 
 -- | Deductions that individuals can claim.
 --
--- The only "special case" field is 'foreignIncomeDeductions', which is
--- the aggregate amount of /other/ deductions that pertains to foreign
--- income.  It is used only for calculating the Foreign Income Tax Offset
--- Limit.
+-- +------------------------------------------------------------------------+----------------------------------------------------------------------------------------+
+-- | 'workRelatedCarExpenses'                                               | __D1__ Work-related car expenses                                                       |
+-- +------------------------------------------------------------------------+----------------------------------------------------------------------------------------+
+-- | 'workRelatedTravelExpenses'                                            | __D2__ Work-related travel expenses                                                    |
+-- +------------------------------------------------------------------------+----------------------------------------------------------------------------------------+
+-- | 'workRelatedClothingLaundryAndDryCleaningExpenses'                     | __D3__ Work-related clothing, laundry and dry-cleaning expenses                        |
+-- +------------------------------------------------------------------------+----------------------------------------------------------------------------------------+
+-- | 'workRelatedSelfEducationExpenses'                                     | __D4__ Work-related self-education expenses                                            |
+-- +------------------------------------------------------------------------+----------------------------------------------------------------------------------------+
+-- | 'otherWorkRelatedExpenses'                                             | __D5__ Other work-related expenses                                                     |
+-- +------------------------------------------------------------------------+----------------------------------------------------------------------------------------+
+-- | 'lowValuePoolDeduction'                                                | __D6__ Low-value pool deduction                                                        |
+-- +------------------------------------------------------------------------+----------------------------------------------------------------------------------------+
+-- | 'interestDeductions'                                                   | __D7__ Interest deductions                                                             |
+-- +------------------------------------------------------------------------+----------------------------------------------------------------------------------------+
+-- | 'dividendDeductions'                                                   | __D8__ Dividend deductions                                                             |
+-- +------------------------------------------------------------------------+----------------------------------------------------------------------------------------+
+-- | 'giftsOrDonations'                                                     | __D9__ Gifts or donations                                                              |
+-- +------------------------------------------------------------------------+----------------------------------------------------------------------------------------+
+-- | 'costOfManagingTaxAffairs'                                             | __D10__ Cost of managing tax affairs                                                   |
+-- +------------------------------------------------------------------------+----------------------------------------------------------------------------------------+
+-- | 'deductibleAmountOfUndeductedPurchasePriceOfAForeignPensionOrAnnuity'  | __D11__ Deductible amount of undeducted purchase price of a foreign pension or annuity |
+-- +------------------------------------------------------------------------+----------------------------------------------------------------------------------------+
+-- | 'personalSuperannuationContributions'                                  | __D12__ Personal superannuation contributions                                          |
+-- +------------------------------------------------------------------------+----------------------------------------------------------------------------------------+
+-- | 'deductionForProjectPool'                                              | __D13__ Deduction for project pool                                                     |
+-- +------------------------------------------------------------------------+----------------------------------------------------------------------------------------+
+-- | 'forestryManagedInvestmentSchemeDeduction'                             | __D14__ Forestry managed investment scheme deduction                                   |
+-- +------------------------------------------------------------------------+----------------------------------------------------------------------------------------+
+-- | 'otherDeductions'                                                      | __D15__ Other deductions — not claimable at __D1__ to __D14__ or elsewhere in your tax |
+-- |                                                                        | return                                                                                 |
+-- +------------------------------------------------------------------------+----------------------------------------------------------------------------------------+
+-- | 'foreignIncomeDeductions'                                              | Aggregate of deductions related to foreign income.  The components making up this      |
+-- |                                                                        | amount __must be included in the other fields__.  This field is only used in           |
+-- |                                                                        | calculating the Foreign Income Tax Offset Limit.                                       |
+-- +------------------------------------------------------------------------+----------------------------------------------------------------------------------------+
 --
 data Deductions a = Deductions
   { _workRelatedCarExpenses :: Money a
@@ -768,7 +881,9 @@
   lens _foreignIncomeDeductions (\s b -> s { _foreignIncomeDeductions = b })
 
 
--- | A gross income (first argument) and amount of tax withheld (second argument)
+-- | A gross income (first argument) and amount of tax withheld (second argument).
+-- The whole gross amount is considered taxable income.
+--
 data GrossAndWithheld a = GrossAndWithheld (Money a) (Money a)
 
 instance (Num a) => Semigroup (GrossAndWithheld a) where
@@ -779,14 +894,34 @@
   mempty = GrossAndWithheld mempty mempty
   mappend = (<>)
 
-instance HasIncome GrossAndWithheld a a where
-  income = to $ \(GrossAndWithheld a _) -> a
+instance HasTaxableIncome GrossAndWithheld a a where
+  taxableIncome = to $ \(GrossAndWithheld a _) -> a
 
 instance HasTaxWithheld GrossAndWithheld a a where
   taxWithheld = to $ \(GrossAndWithheld _ a) -> a
 
 
 -- | Employee share scheme statement.  Use 'newESSStatement' to construct.
+-- The following lenses are available:
+--
+-- +------------------------------+--------------------------------------------+
+-- | 'essTaxedUpfrontReduction'   | __D__ Discount from taxed up front         |
+-- |                              | schemes—eligible for reduction             |
+-- +------------------------------+--------------------------------------------+
+-- | 'essTaxedUpfrontNoReduction' | __E__ Discount from taxed up front         |
+-- |                              | schemes—not eligible for reduction         |
+-- +------------------------------+--------------------------------------------+
+-- | 'essDeferral'                | __F__ Discount from taxed deferral schemes |
+-- +------------------------------+--------------------------------------------+
+-- | 'essPre2009'                 | __G__ Discounts on ESS interests acquired  |
+-- |                              | pre 1 July 2009 and "cessation time"       |
+-- |                              | occurred during the financial year.        |
+-- +------------------------------+--------------------------------------------+
+-- | 'essTFNAmounts'              | __C__ TFN amounts withheld from discounts  |
+-- +------------------------------+--------------------------------------------+
+-- | 'essForeignSourceDiscounts'  | __A__ ESS foreign source discounts         |
+-- +------------------------------+--------------------------------------------+
+--
 data ESSStatement a = ESSStatement
   { _taxedUpfrontReduction :: Money a
   , _taxedUpfrontNoReduction :: Money a
@@ -836,8 +971,8 @@
 
 -- | __Note:__ does not implement the reduction of taxed up front
 -- amounts eligible for reduction.
-instance (Num a) => HasIncome ESSStatement a a where
-  income = to $ \s ->
+instance (Num a) => HasTaxableIncome ESSStatement a a where
+  taxableIncome = to $ \s ->
     view essTaxedUpfrontReduction s
     <> view essTaxedUpfrontNoReduction s
     <> view essDeferral s
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
@@ -75,6 +75,13 @@
   }
   deriving (Show)
 
+instance Functor CGTEvent where
+  fmap f (CGTEvent k n t1 p1 b1 t2 p2 b2 cap own) =
+    CGTEvent k (f n)
+      t1 (f <$> p1) (f <$> b1)
+      t2 (f <$> p2) (f <$> b2)
+      (f <$> cap) (f <$> own)
+
 reducedCostBase :: Num a => CGTEvent a -> Money a
 reducedCostBase event =
   (units event *$ acquisitionPrice event)
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
@@ -30,11 +30,10 @@
     TaxTables(..)
 
   -- * Classes
-  , HasIncome(..)
+  , HasTaxableIncome(..)
 
   -- * Common taxes and helpers
   , medicareLevy
-  , medicareLevySurcharge
   , lowIncomeTaxOffset
   , lowIncomeTaxOffset2021
   , lamito
@@ -70,18 +69,6 @@
 medicareLevy :: (Fractional a, Ord a) => Money a -> Tax (Money a) (Money a)
 medicareLevy l = lesserOf (above l 0.1) (flat 0.02)
 
--- | /Medicare levy surcharge (MLS)/.  Certain exemptions are available.
---
--- __Known issues__: the MLS is levied on taxable income + fringe
--- benefits, but this is not implemented properly yet.  The
--- thresholds are affected by family income and number of
--- dependents; this also is not implemented.
-medicareLevySurcharge :: (Fractional a, Ord a) => Tax (Money a) (Money a)
-medicareLevySurcharge =
-  threshold (review money 90000) 0.01
-  <> threshold (review money 105000) 0.0025
-  <> threshold (review money 140000) 0.0025
-
 -- | /Low income tax offset (LITO)/.  $445, reduced by 1.5c for
 -- every dollar earned over $37,000. The lump amount may change in
 -- the future.
@@ -120,9 +107,12 @@
 -- ^ Convenience wrapper for 'marginal'.  Turns the margins into 'Money'
 
 
--- | Types that have an income value.
-class HasIncome a b c where
-  income :: Getter (a b) (Money c)
+-- | Types that may have a taxable income component.
+class HasTaxableIncome a b c where
+  taxableIncome :: Getter (a b) (Money c)
 
-instance (Foldable t, HasIncome x a a, Num a) => HasIncome t (x a) a where
-  income = to (foldMap (view income))
+instance
+    (Foldable t, HasTaxableIncome x a a, Num a)
+    => HasTaxableIncome t (x a) a
+    where
+  taxableIncome = to (foldMap (view taxableIncome))
diff --git a/src/Data/Tax/ATO/Days.hs b/src/Data/Tax/ATO/Days.hs
deleted file mode 100644
--- a/src/Data/Tax/ATO/Days.hs
+++ /dev/null
@@ -1,76 +0,0 @@
--- This file is part of hs-tax-ato
--- Copyright (C) 2018  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/>.
-
-{-|
-
-Types for representing a number of days in a year.
-
--}
-
-{-# LANGUAGE ConstraintKinds #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE KindSignatures #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-
-module Data.Tax.ATO.Days
-  ( Days
-  , days
-  , daysAll
-  , daysNone
-  , getDays
-  , getFraction
-  , DaysInYear
-  )
-  where
-
-import GHC.TypeLits
-import Data.Proxy
-import Data.Ratio ((%))
-
-import Data.Time.Calendar (isLeapYear)
-
-type Year = Nat
-type DaysInYear = KnownNat
-
-daysInYear :: KnownNat n => Proxy n -> Integer
-daysInYear proxy
-  | isLeapYear (natVal proxy) = 366
-  | otherwise                 = 365
-
--- | Some number of days in a year.  Use 'days' to construct.
-newtype Days (n :: Year) = Days
-  { getDays :: Integer
-  -- ^ Get the number of days, which is between 0 and 365/366 inclusive.
-  }
-  deriving (Show)
-
--- | 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. (DaysInYear a) => Integer -> Days a
-days = Days . max 0 . min (daysInYear (Proxy :: Proxy a))
-
--- | Every day of the year
-daysAll :: forall a. (DaysInYear a) => Days a
-daysAll = Days (daysInYear (Proxy :: Proxy a))
-
--- | Zero days of the year
-daysNone :: Days a
-daysNone = Days 0
-
--- | Get the number of days as a fractional value.
--- Information about the the year type is discarded.
-getFraction :: forall a frac. (DaysInYear a, Fractional frac) => Days a -> frac
-getFraction n = fromRational $ getDays n % daysInYear (Proxy :: Proxy a)
diff --git a/src/Data/Tax/ATO/FY.hs b/src/Data/Tax/ATO/FY.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Tax/ATO/FY.hs
@@ -0,0 +1,100 @@
+-- This file is part of hs-tax-ato
+-- Copyright (C) 2018, 2023  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/>.
+
+{-|
+
+Types and functions related to financial years.
+
+-}
+
+{-# LANGUAGE ConstraintKinds #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE KindSignatures #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Data.Tax.ATO.FY
+  ( Days
+  , days
+  , daysAll
+  , daysNone
+  , getDays
+  , getFraction
+  , FinancialYear
+  , financialYear
+  , financialYearRange
+  , financialYearRangeFromProxy
+  )
+  where
+
+import GHC.TypeLits
+import Data.Proxy
+import Data.Ratio ((%))
+
+import Data.Time.Calendar (Day, Year, fromGregorian, isLeapYear, toGregorian)
+
+type FinancialYear = KnownNat
+
+daysInYear :: KnownNat n => Proxy n -> Integer
+daysInYear proxy
+  | isLeapYear (natVal proxy) = 366
+  | otherwise                 = 365
+
+-- | Some number of days in a year.  Use 'days' to construct.
+newtype Days (n :: Nat) = Days
+  { getDays :: Integer
+  -- ^ Get the number of days, which is between 0 and 365/366 inclusive.
+  }
+  deriving (Show)
+
+-- | 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))
+
+-- | Every day of the year
+daysAll :: forall a. (FinancialYear a) => Days a
+daysAll = Days (daysInYear (Proxy :: Proxy a))
+
+-- | Zero days of the year
+daysNone :: Days a
+daysNone = Days 0
+
+-- | Get the number of days as a fractional value.
+-- 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)
+
+-- | The financial year in which the given day falls.
+--
+financialYear :: Day -> Year
+financialYear d = case toGregorian d of
+  (y, m, _)
+    | m >= 7{-July-}  -> y + 1
+    | otherwise       -> y
+
+-- | Get the range of days (inclusive) for the financial year (July to June)
+-- ending in the given year.
+financialYearRange :: Year -> (Day, Day)
+financialYearRange y =
+  ( fromGregorian (y - 1) 7{-July-} 1
+  , fromGregorian y       6{-June-} 30
+  )
+
+-- | Get the financial year range (inclusive) for the given type-level
+-- financial year.  See also 'financialYearRange'.
+financialYearRangeFromProxy :: forall n. (FinancialYear n) => Proxy n -> (Day, Day)
+financialYearRangeFromProxy proxy = financialYearRange (natVal proxy)
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
@@ -43,6 +43,19 @@
   , (82551, 0.005), (86895, 0.005), (95627, 0.005), (101900, 0.005) ]
 sfss = thresholds' [(54869, 0.02), (67369, 0.01), (95627, 0.01)]
 
+-- | /Medicare levy surcharge (MLS)/.  Certain exemptions are available.
+--
+-- __Known issues__: the MLS is levied on taxable income + fringe
+-- benefits, but this is not implemented properly yet.  The
+-- thresholds are affected by family income and number of
+-- dependents; this also is not implemented.
+--
+medicareLevySurcharge :: (Fractional a, Ord a) => Tax (Money a) (Money a)
+medicareLevySurcharge =
+  threshold (Money 90000) 0.01
+  <> threshold (Money 105000) 0.0025
+  <> threshold (Money 140000) 0.0025
+
 -- | In FY2017 the 37% threshold was raised from $80,000 to $87,000
 --
 -- The Temporary Budget Repair Levy, 2% of income above $180,000,
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
@@ -17,14 +17,19 @@
 {-# LANGUAGE DataKinds #-}
 
 -- | Tax tables for 2017–18 financial year.
-module Data.Tax.ATO.FY.FY2018 (tables) where
+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
 import qualified Data.Tax.ATO.FY.FY2017 as FY2017
 
+type FY = 2018
+fyProxy :: Proxy FY
+fyProxy = Proxy
+
 help, sfss :: (Fractional a, Ord a) => Tax (Money a) (Money a)
 help = thresholds'
   [ (55874, 0.04)
@@ -36,11 +41,11 @@
 --
 -- The /temporary budget repair levy/ no longer applies.
 --
-tables :: (Ord a, Fractional a) => TaxTables 2018 a
+tables :: (Ord a, Fractional a) => TaxTables FY a
 tables = TaxTables
   FY2017.individualIncomeTax
   (medicareLevy (review money 21980))
-  medicareLevySurcharge
+  (ttMedicareLevySurcharge FY2017.tables)
   help
   sfss
   lowIncomeTaxOffset
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
@@ -17,14 +17,20 @@
 {-# LANGUAGE DataKinds #-}
 
 -- | Tax tables for 2018–19 financial year.
-module Data.Tax.ATO.FY.FY2019 (tables) where
+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
 import Data.Tax.ATO.PrivateHealthInsuranceRebate
+import qualified Data.Tax.ATO.FY.FY2018 as FY2018
 
+type FY = 2019
+fyProxy :: Proxy FY
+fyProxy = Proxy
+
 -- | In FY2019 the 37% threshold was increased from $87,000 to $90,000.
 individualIncomeTax :: (Fractional a, Ord a) => Tax (Money a) (Money a)
 individualIncomeTax = marginal'
@@ -45,11 +51,11 @@
 -- The new /low and middle income tax offset (LAMITO)/ was
 -- introduced, in addition to LITO.
 --
-tables :: (Ord a, Fractional a) => TaxTables 2019 a
+tables :: (Ord a, Fractional a) => TaxTables FY a
 tables = TaxTables
   individualIncomeTax
   (medicareLevy (review money 22398))
-  medicareLevySurcharge
+  (ttMedicareLevySurcharge FY2018.tables)
   help
   sfss
   (lowIncomeTaxOffset <> lamito)
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
@@ -17,8 +17,9 @@
 {-# LANGUAGE DataKinds #-}
 
 -- | Tax tables for 2019–20 financial year.
-module Data.Tax.ATO.FY.FY2020 (tables) where
+module Data.Tax.ATO.FY.FY2020 (FY, fyProxy, tables) where
 
+import Data.Proxy
 import Control.Lens (review)
 
 import Data.Tax
@@ -26,6 +27,10 @@
 import Data.Tax.ATO.PrivateHealthInsuranceRebate
 import qualified Data.Tax.ATO.FY.FY2019 as FY2019
 
+type FY = 2020
+fyProxy :: Proxy FY
+fyProxy = Proxy
+
 help :: (Fractional a, Ord a) => Tax (Money a) (Money a)
 help = thresholds'
   [ (45881, 0.01)
@@ -52,11 +57,11 @@
 -- one set of thresholds and rates.  For backwards compatibility,
 -- 'ttHelp' and 'ttSfss' now refer to the same value.
 --
-tables :: (Ord a, Fractional a) => TaxTables 2020 a
+tables :: (Ord a, Fractional a) => TaxTables FY a
 tables = TaxTables
   (ttIndividualIncomeTax FY2019.tables)
   (medicareLevy (review money 22801))
-  medicareLevySurcharge
+  (ttMedicareLevySurcharge FY2019.tables)
   help
   help
   (lowIncomeTaxOffset <> lamito)
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
@@ -17,14 +17,20 @@
 {-# LANGUAGE DataKinds #-}
 
 -- | Tax tables for 2020–21 financial year.
-module Data.Tax.ATO.FY.FY2021 (tables, individualIncomeTax) where
+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
 import Data.Tax.ATO.PrivateHealthInsuranceRebate
+import qualified Data.Tax.ATO.FY.FY2020 as FY2020
 
+type FY = 2021
+fyProxy :: Proxy FY
+fyProxy = Proxy
+
 -- | In 2020–21 the 32.5% threshold was increased from $37,000 to
 -- $45,000, and the 37% threshold was increased from $90,000 to
 -- $120,000.
@@ -57,11 +63,11 @@
   , (136740, 0.005)
   ]
 
-tables :: (Ord a, Fractional a) => TaxTables 2021 a
+tables :: (Ord a, Fractional a) => TaxTables FY a
 tables = TaxTables
   individualIncomeTax
   (medicareLevy (review money 23226))
-  medicareLevySurcharge
+  (ttMedicareLevySurcharge FY2020.tables)
   help
   help
   (lowIncomeTaxOffset2021 <> lamito)
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
@@ -17,8 +17,9 @@
 {-# LANGUAGE DataKinds #-}
 
 -- | Tax tables for 2021–22 financial year.
-module Data.Tax.ATO.FY.FY2022 (tables) where
+module Data.Tax.ATO.FY.FY2022 (FY, fyProxy, tables) where
 
+import Data.Proxy
 import Control.Lens (review)
 
 import Data.Tax
@@ -26,6 +27,10 @@
 import Data.Tax.ATO.PrivateHealthInsuranceRebate
 import qualified Data.Tax.ATO.FY.FY2021 as FY2021
 
+type FY = 2022
+fyProxy :: Proxy FY
+fyProxy = Proxy
+
 help :: (Fractional a, Ord a) => Tax (Money a) (Money a)
 help = thresholds'
   [ (47014, 0.01)
@@ -48,11 +53,11 @@
   , (137898, 0.005)
   ]
 
-tables :: (Ord a, Fractional a) => TaxTables 2022 a
+tables :: (Ord a, Fractional a) => TaxTables FY a
 tables = TaxTables
   FY2021.individualIncomeTax
   (medicareLevy (review money 23365))
-  medicareLevySurcharge
+  (ttMedicareLevySurcharge FY2021.tables)
   help
   help
   (lowIncomeTaxOffset2021 <> lmito2022)
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
@@ -17,12 +17,17 @@
 {-# LANGUAGE DataKinds #-}
 
 -- | Tax tables for 2022–23 financial year.
-module Data.Tax.ATO.FY.FY2023 (tables) where
+module Data.Tax.ATO.FY.FY2023 (FY, fyProxy, tables) where
 
+import Data.Proxy
 import Data.Tax
 import Data.Tax.ATO.Common
 import qualified Data.Tax.ATO.FY.FY2022 as FY2022
 
+type FY = 2023
+fyProxy :: Proxy FY
+fyProxy = Proxy
+
 help :: (Fractional a, Ord a) => Tax (Money a) (Money a)
 help = thresholds'
   [ (48361, 0.01)
@@ -50,8 +55,8 @@
   (ttIndividualIncomeTax FY2022.tables)
 
   (medicareLevy (Money 24276))
+  (ttMedicareLevySurcharge FY2022.tables)
 
-  medicareLevySurcharge
   help
   help
 
diff --git a/src/Data/Tax/ATO/FY/FY2024.hs b/src/Data/Tax/ATO/FY/FY2024.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Tax/ATO/FY/FY2024.hs
@@ -0,0 +1,74 @@
+-- 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 2023–24 financial year.
+module Data.Tax.ATO.FY.FY2024 (FY, fyProxy, tables) where
+
+import Data.Proxy
+import Data.Tax
+import Data.Tax.ATO.Common
+import qualified Data.Tax.ATO.FY.FY2023 as FY2023
+
+type FY = 2024
+fyProxy :: Proxy FY
+fyProxy = Proxy
+
+help :: (Fractional a, Ord a) => Tax (Money a) (Money a)
+help = thresholds'
+  [ (51550, 0.01)
+  , (59519, 0.01)
+  , (63090, 0.005)
+  , (66876, 0.005)
+  , (70889, 0.005)
+  , (75141, 0.005)
+  , (79650, 0.005)
+  , (84430, 0.005)
+  , (89495, 0.005)
+  , (94866, 0.005)
+  , (100558, 0.005)
+  , (106591, 0.005)
+  , (112986, 0.005)
+  , (119765, 0.005)
+  , (126951, 0.005)
+  , (134569, 0.005)
+  , (142643, 0.005)
+  , (151201, 0.005)
+  ]
+
+-- | Medicare levy surcharge thresholds changed for 2023–24
+medicareLevySurcharge :: (Fractional a, Ord a) => Tax (Money a) (Money a)
+medicareLevySurcharge =
+  threshold (Money 93000) 0.01
+  <> threshold (Money 108000) 0.0025
+  <> threshold (Money 144000) 0.0025
+
+tables :: (Ord a, Fractional a) => TaxTables FY a
+tables = TaxTables
+  (ttIndividualIncomeTax FY2023.tables)
+
+  (medicareLevy (Money 26000))
+
+  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)
diff --git a/src/Data/Tax/ATO/Pretty.hs b/src/Data/Tax/ATO/Pretty.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Tax/ATO/Pretty.hs
@@ -0,0 +1,167 @@
+-- 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/>.
+
+{-|
+
+Pretty-print tax data.
+
+Monetary values are rounded to the nearest whole cent (half-up).
+
+-}
+
+{-# LANGUAGE OverloadedStrings #-}
+
+module Data.Tax.ATO.Pretty
+  ( summariseTaxReturnInfo
+  , summariseAssessment
+  , summariseCGTAssessment
+  ) where
+
+import Data.Function (on)
+import Data.List (groupBy, sortOn)
+
+import Control.Lens (ALens', cloneLens, view, views)
+import qualified Text.PrettyPrint as P
+
+import Data.Tax.ATO
+import Data.Tax.ATO.CGT
+
+
+colWidthMoney, colWidthLabel :: Int
+colWidthMoney = 16
+colWidthLabel = 80 - colWidthMoney
+
+-- | Format money for display.  Rounds to the nearest whole cent (half-up).
+-- Does not prepend '$'.
+formatMoney :: Money Rational -> P.Doc
+formatMoney (Money x) =
+  P.text (replicate (colWidthMoney - length amount) ' ' <> amount)
+  where
+    (iPart, fPart) = properFraction (x * 100) :: (Integer, Rational)
+    iPart' = if fPart >= 0.5 then iPart + 1 else iPart
+    digits = reverse (show iPart')
+    cents = case reverse (take 2 digits) of [] -> "00" ; [c] -> c:"0" ; s -> s
+    dollars = case drop 2 digits of "" -> "0" ; s -> reverse (putCommas s)
+    amount = dollars <> "." <> cents
+    putCommas (a:b:c:d:rest) | d /= '-' = a:b:c:',':putCommas (d:rest)
+    putCommas rest                      = rest
+
+twoCol :: (P.Doc, Money Rational) -> P.Doc
+twoCol (label, value) = label P.$$ P.nest (80 - colWidthMoney) (formatMoney value)
+
+threeCol :: (P.Doc, Money Rational, Money Rational) -> P.Doc
+threeCol (label, v1, v2) =
+  label
+  P.$$ P.nest (80 - 2 * colWidthMoney) (formatMoney v1)
+  P.<> formatMoney v2
+
+-- | 3-column layout with rightmost column blank
+threeColLeft :: (P.Doc, Money Rational) -> P.Doc
+threeColLeft (label, v1) =
+  label
+  P.$$ P.nest (80 - 2 * colWidthMoney) (formatMoney v1)
+
+vcatWith :: (a -> P.Doc) -> [a] -> P.Doc
+vcatWith f = P.vcat . fmap f
+
+summariseTaxReturnInfo :: TaxReturnInfo y Rational -> P.Doc
+summariseTaxReturnInfo info =
+  "Income"
+  P.$+$ vcatWith threeCol
+    [ ("  1   Salary or wages"  , view (paymentSummaries . taxWithheld) info, view (paymentSummaries . taxableIncome) info)
+    , ("  10  Interest"         , view (interest . taxWithheld) info, view (interest . taxableIncome) info)
+    ]
+  P.$+$ "  11  Dividends"
+  P.$+$ views dividends summariseDividends info
+  P.$+$ vcatWith twoCol
+    [ ("  12  Employee share schemes" , view (ess . taxableIncome) info)
+    , ("  20M Other net foreign source income" , view foreignIncome info)
+    ]
+  P.$+$ "Deductions"
+  P.$+$ P.vcat (uncurry (summariseDeduction (view deductions info)) <$> deductionsTable)
+  P.$+$ "Tax offsets"
+  P.$+$ vcatWith threeColLeft
+    [ ("  20O Foreign income tax offset"  , view (offsets . foreignTaxOffset) info)
+    ]
+
+summariseDividends :: [Dividend Rational] -> P.Doc
+summariseDividends =
+  vcatWith (threeCol . prep)
+  . groupBy ((==) `on` dividendSource)
+  . sortOn dividendSource
+  where
+    prep :: [Dividend Rational] -> (P.Doc, Money Rational, Money Rational)
+    prep l =
+      ( P.text ("        " <> dividendSource (head l))
+      , view taxWithheld l
+      , view taxableIncome l
+      )
+
+deductionsTable :: [(ALens' (Deductions Rational) (Money Rational), String)]
+deductionsTable =
+  [ (workRelatedCarExpenses, "D1  Work-related car expenses")
+  , (workRelatedTravelExpenses, "D2  Work-related travel expenses")
+  , (workRelatedClothingLaundryAndDryCleaningExpenses, "D3  Work-related clothing, laundry and dry cleaning expenses")
+  , (workRelatedSelfEducationExpenses, "D4  Work-related self-education expenses")
+  , (otherWorkRelatedExpenses, "D5  Other work-related expenses")
+  , (lowValuePoolDeduction, "D6  Low value pool deduction")
+  , (interestDeductions, "D7  Interest deductions")
+  , (dividendDeductions, "D8  Dividend deductions")
+  , (giftsOrDonations, "D9  Gifts or donations")
+  , (costOfManagingTaxAffairs, "D10 Cost of managing tax affairs")
+  , (deductibleAmountOfUndeductedPurchasePriceOfAForeignPensionOrAnnuity, "D11 Deductible amount of undeducted purchase price of a foreign pension or annuity")
+  , (personalSuperannuationContributions, "D12 Personal superannuation contributions")
+  , (deductionForProjectPool, "D13 Deduction for project pool")
+  , (forestryManagedInvestmentSchemeDeduction, "D14 Forestry managed investment scheme deduction")
+  , (otherDeductions, "D15 Other deductions")
+  ]
+
+summariseDeduction
+  :: Deductions Rational
+  -> ALens' (Deductions Rational) (Money Rational)
+  -> 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
+
+
+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.$+$ 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
@@ -27,6 +27,8 @@
   , PrivateHealthInsurancePolicyDetail(..)
   , BenefitCode(..)
   , assessExcessPrivateHealthRebate
+  , HealthInsurerID
+  , MembershipNumber
   ) where
 
 import Data.List (find)
@@ -40,12 +42,14 @@
 
 data BenefitCode
   = BenefitCode30 -- ^ Under 65, 1 July to 31 March
-  | BenefitCode31 -- ^ Over 65, 1 April to 30 June
+  | BenefitCode31 -- ^ Under 65, 1 April to 30 June
   | BenefitCode35 -- ^ 65 to 69, 1 July to 31 March
   | BenefitCode36 -- ^ 65 to 69, 1 April to 30 June
   | BenefitCode40 -- ^ 70 or over, 1 July to 31 March
   | BenefitCode41 -- ^ 70 or over, 1 April to 30 June
 
+-- | Include these data in your tax return via the
+-- 'Data.Tax.ATO.privateHealthInsurancePolicyDetails' field.
 data PrivateHealthInsurancePolicyDetail a =
   PrivateHealthInsurancePolicyDetail
     HealthInsurerID
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:             2023.2
+version:             2024.1
 synopsis:            Tax types and computations for Australia
 description:
   This library provides types and tax computations for tax
@@ -14,10 +14,11 @@
 copyright:           Copyright (C) 2018-2023 Fraser Tweedale
 category:            Finance
 build-type:          Simple
-tested-with:         GHC ==8.10.7 || ==9.0.2 || ==9.2.7 || ==9.4.4 || ==9.6.1
+tested-with:         GHC ==9.2.8 || ==9.4.7 || ==9.6.3 || ==9.8.1
 extra-source-files:
-  CHANGELOG.md
   .hlint.yaml
+extra-doc-files:
+  CHANGELOG.md
 
 homepage:            https://github.com/frasertweedale/hs-tax-ato
 bug-reports:         https://github.com/frasertweedale/hs-tax-ato/issues
@@ -62,9 +63,10 @@
     Data.Tax.ATO
     Data.Tax.ATO.CGT
     Data.Tax.ATO.Common
-    Data.Tax.ATO.Days
+    Data.Tax.ATO.Pretty
     Data.Tax.ATO.PrivateHealthInsuranceRebate
     Data.Tax.ATO.Rounding
+    Data.Tax.ATO.FY
     Data.Tax.ATO.FY.FY2017
     Data.Tax.ATO.FY.FY2018
     Data.Tax.ATO.FY.FY2019
@@ -72,7 +74,9 @@
     Data.Tax.ATO.FY.FY2021
     Data.Tax.ATO.FY.FY2022
     Data.Tax.ATO.FY.FY2023
+    Data.Tax.ATO.FY.FY2024
   build-depends:
     , lens >= 4.12 && < 6
-    , time >= 1.5 && < 1.13
+    , time >= 1.11 && < 1.13
     , tax >= 0.2 && < 0.3
+    , pretty >= 1.1.3.6 && < 2
