diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,97 @@
+## Version 2026.1
+
+### CGT overhaul
+
+Capital gains tax (CGT) received a significant overhaul.
+Enhancements include:
+
+- Added the a `CGTAsset` data type, which includes `CGTAssetType`
+  for tracking different categories of CGT assets.
+
+- Implemented a separate loss carry-forward for collectables.  The
+  new `CapitalLossCarryForward` data type has separate fields for
+  capital losses on collectables and other capital losses.
+
+- Capital losses on personal use assets (`AssetTypePersonalUse`) are
+  disregarded.
+
+- Added the `EntityType` data type.  This ensures the correct CGT
+  discount proportion is used depending on whether the taxed entity
+  is an `Individual`, `Company`, `Trust` or `SuperFund`.
+
+- The `assessCGTEvents` function now has an `EntityType` argument.
+
+- Capital gains from trusts can now be included in CGT calculations.
+  The `CapitalGainsFromTrust` data type represents capital gains
+  from trust distributions.  The 'prepTrustCGT' function converts
+  `CapitalGainsFromTrust` values into CGT events that represent the
+  grossed-up gain, including eligibility for the CGT discount or
+  small business concession.
+
+- Added convenience constructors for various kinds of CGT events.
+
+- Added some data types that will support the implementation (in a
+  future release) of various CGT exemptions and small business CGT
+  concessions.
+
+- The `Data.Tax.ATO.Pretty.summariseCGTSchedule` prints the *CGT
+  schedule* for a given `CGTAssessment`.
+
+### Personal services income (PSI)
+
+Added module `Data.Tax.ATO.BPI` (*Business and professional items*),
+which provides for declaring personal services income and associated
+deductions.
+
+Usage:
+
+```haskell
+info =
+  newTaxReturnInfoForTables tables
+  & set
+      ( businessAndProfessionalItems
+      . personalServicesIncome
+      . psiOther )
+      ( Money 18000 )
+  & set
+      ( businessAndProfessionalItems
+      . personalServicesIncome
+      . psiTotalAmountOfOtherDeductions
+      )
+      ( Money  2000 )
+```
+
+### "Other income"
+
+Added the `otherIncome` field and the `OtherIncome` data type, which
+contains a number of fields for declaring various kinds of income
+(including FHSS released amounts).
+
+Example usage:
+
+```haskell
+info =
+  newTaxReturnInfoForTables tables
+  & set (otherIncome . otherIncomeCategory4) [("Airdrop", Money 5000)]
+```
+
+### Other features and enhancements
+
+- Add `FY2026` module.  Apart from the usual Medicare levy and
+  private health insurance rebate adjustments, study loan repayments
+  changed to a marginal tax (previously a threshold tax).
+
+- Add preliminary `FY2027` module.  Values are not final.  The
+  new *$1,000 instant deduction* is not yet implemented.
+
+### Bug fixes
+
+- FY2020+ now calculate study and training loan repayment correctly
+  where individual has both HELP and SFSS balances.
+
+- Pretty-printers updated to omit various line items when zero.
+
+
 ## Version 2025.1
 
 ### Features and enhancements
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
@@ -60,6 +60,8 @@
 
   -- *** Capital gains tax (CGT)
   , HasCapitalLossCarryForward(..)
+  , capitalLossCarryForwardOther
+  , capitalLossCarryForwardCollectables
   , cgtEvents
 
   -- *** Employee share schemes
@@ -74,9 +76,24 @@
   , essTFNAmounts
   , essForeignSourceDiscounts
 
+  -- ***Business and professional items***
+  , businessAndProfessionalItems
+  , module Data.Tax.ATO.BPI
+
   -- *** Foreign income
   , foreignIncome
 
+  -- *** Other income
+  , OtherIncome
+  , otherIncome
+  , otherIncomeCategory1
+  , otherIncomeCategory2
+  , otherIncomeCategory3
+  , otherIncomeCategory4
+  , taxWithheldLumpSumPaymentsInArrears
+  , taxWithheldAssessableFHSSReleasedAmount
+  , taxableProfessionalIncome
+
   -- ** Medicare Levy Surcharge and Private Health Insurance
   , mlsExemption
   , privateHealthInsurancePolicyDetails
@@ -177,12 +194,18 @@
   , module Data.Tax.ATO.Rounding
   ) where
 
-import Control.Lens (Getter, Lens', (&), foldOf, lens, preview, set, to, view, views)
+import Data.Proxy
+
+import Control.Lens
+  ( Getter, Lens'
+  , (&), _2, 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.BPI
+import Data.Tax.ATO.CGT
 import Data.Tax.ATO.Common
 import Data.Tax.ATO.Depreciation
 import Data.Tax.ATO.FY
@@ -316,48 +339,56 @@
 --
 -- The following lenses are available:
 --
--- +------------------------------------------------------+----------------------------------+
--- | '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                     |
--- +------------------------------------------------------+----------------------------------+
+-- +------------------------------------------------------+----------------------------------------+
+-- | '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        |
+-- +------------------------------------------------------+----------------------------------------+
+-- | 'otherIncome'                                        | /Other income/ fields                  |
+-- +------------------------------------------------------+----------------------------------------+
+-- | 'businessAndProfessionalItems'                       | See                                    |
+-- |                                                      | 'BusinessAndProfessionalItemsSchedule' |
+-- +------------------------------------------------------+----------------------------------------+
+-- | 'foreignIncome'                                      | Foreign income                         |
+-- +------------------------------------------------------+----------------------------------------+
+-- | 'cgtEvents' :: ['CGTEvent']                          | Capital gains tax events for           |
+-- |                                                      | this income year                       |
+-- +------------------------------------------------------+----------------------------------------+
+-- | 'capitalLossCarryForward'                            | Capital losses carried forward         |
+-- |   :: 'CapitalLossCarryForward'                       | from previous income years             |
+-- +------------------------------------------------------+----------------------------------------+
+-- | '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
@@ -370,11 +401,13 @@
   , _interest :: GrossAndWithheld a
   , _dividends :: [Dividend a]
   , _ess :: [ESSStatement a]
+  , _otherIncome :: OtherIncome a
+  , _businessAndProfessionalItems :: BusinessAndProfessionalItemsSchedule a
   , _foreignIncome :: Money a
   , _cgtEvents :: [CGTEvent a]
   , _deductions :: Deductions a
   , _offsets :: Offsets a
-  , _triCapitalLossCarryForward :: Money a
+  , _triCapitalLossCarryForward :: CapitalLossCarryForward a
   , _phi :: [PrivateHealthInsurancePolicyDetail a]
   , _spouseDetails :: Maybe (SpouseDetails a)
   , _incomeTests :: IncomeTests a
@@ -402,11 +435,13 @@
   mempty -- interest
   mempty -- dividends
   mempty -- ESS
+  newOtherIncome
+  newBusinessAndProfessionalItemsSchedule
   mempty -- foreign income
   mempty -- CGT events
   mempty -- deductions
   mempty -- offsets
-  mempty -- cap loss carry forward
+  newCapitalLossCarryForward
   mempty -- private health insurance policy details
   Nothing -- spouse details
   newIncomeTests
@@ -467,9 +502,17 @@
 dividends :: Lens' (TaxReturnInfo y a) [Dividend a]
 dividends = lens _dividends (\s b -> s { _dividends = b })
 
+businessAndProfessionalItems
+  :: Lens' (TaxReturnInfo y a) (BusinessAndProfessionalItemsSchedule a)
+businessAndProfessionalItems =
+  lens _businessAndProfessionalItems (\s b -> s { _businessAndProfessionalItems = b })
+
 ess :: Lens' (TaxReturnInfo y a) [ESSStatement a]
 ess = lens _ess (\s b -> s { _ess = b })
 
+otherIncome :: Lens' (TaxReturnInfo y a) (OtherIncome a)
+otherIncome = lens _otherIncome (\s b -> s { _otherIncome = b })
+
 foreignIncome :: Lens' (TaxReturnInfo y a) (Money a)
 foreignIncome = lens _foreignIncome (\s b -> s { _foreignIncome = b })
 
@@ -592,13 +635,16 @@
 -- | Tax to calculate compulsory study and training loan repayments
 -- (e.g. HELP, SFSS)
 studyAndTrainingLoanRepaymentTax
-  :: (Fractional a, Ord a)
+  :: forall y a. (FinancialYear y, Fractional a, Ord a)
   => TaxTables y a
   -> TaxReturnInfo y a
   -> Tax (Money a) (Money a)
-studyAndTrainingLoanRepaymentTax table info =
-  limit (view helpBalance info) (ttHelp table)
-  <> limit (view sfssBalance info) (ttSfss table)
+studyAndTrainingLoanRepaymentTax table info = case fromProxy (Proxy @y) of
+  y | y < 2020 ->
+        limit (view helpBalance info) (ttHelp table)
+        <> limit (view sfssBalance info) (ttSfss table)
+    | otherwise ->
+        limit (view helpBalance info <> view sfssBalance info) (ttHelp table)
 
 -- | Taxable income
 instance (RealFrac a) => HasTaxableIncome (TaxReturnInfo y) a a where
@@ -613,7 +659,9 @@
         , view (interest . taxableIncome) info
         , view (dividends . taxableIncome) info
         , view (ess . taxableIncome) info
-        , view (cgtEvents . to (assessCGTEvents cf) . cgtNetGain) info
+        , view (otherIncome . taxableIncome) info
+        , view (businessAndProfessionalItems . taxableIncome) info
+        , view (cgtEvents . to (assessCGTEvents Individual cf) . cgtNetGain) info
         , view foreignIncome info
         ]
     in
@@ -632,6 +680,7 @@
     <> view (paymentSummariesWithholdingWhereABNNotQuoted . taxWithheld) info
     <> view (interest . taxWithheld) info
     <> view (ess . taxWithheld) info
+    <> view (otherIncome . taxWithheld) info
 
 fringeBenefits :: (Num a) => TaxReturnInfo y a -> Money a
 fringeBenefits info =
@@ -665,7 +714,17 @@
   -- TODO super lump sum taxed elements with zero tax rate
   -- TODO assessable FHSS released amount
 
-paygInstalmentIncome :: (RealFrac a) => TaxReturnInfo y a -> Money a
+-- | PAYG instalment income includes:
+--
+-- * Dividends and interest payments
+-- * Foreign income
+-- * Payments where amounts withheld due to non-quotation of TFN or ABN
+--
+-- It also includes rent, business income and partnership/trust income, but
+-- this library does not yet implement these features so these amounts are
+-- are not reflected in this calculation.
+--
+paygInstalmentIncome :: forall y a. (RealFrac a) => TaxReturnInfo y a -> Money a
 paygInstalmentIncome info =
   -- TODO gross rent
   view (dividends . taxableIncome) info
@@ -686,7 +745,7 @@
   => TaxTables y a -> TaxReturnInfo y a -> TaxAssessment a
 assessTax tables info =
   let
-    cg = assessCGTEvents
+    cg = assessCGTEvents Individual
           (view capitalLossCarryForward info) (view cgtEvents info)
     taxable = view taxableIncome info
     due = getTax (individualTax tables) taxable
@@ -1210,3 +1269,67 @@
 
 instance HasTaxWithheld ESSStatement a a where
   taxWithheld = essTFNAmounts
+
+
+-- | __24__ Other income
+data OtherIncome a = OtherIncome
+  { _category1 :: [(String, Money a)]
+  , _category2 :: [(String, Money a)]
+  , _category3 :: Money a
+  , _category4 :: [(String, Money a)]
+  , _taxWithheldLSPIA :: Money a
+  , _taxWithheldFHSS :: Money a
+  , _taxableProfessionalIncome :: Money a
+  }
+
+newOtherIncome :: (Num a) => OtherIncome a
+newOtherIncome = OtherIncome
+  { _category1 = []
+  , _category2 = []
+  , _category3 = mempty
+  , _category4 = []
+  , _taxWithheldLSPIA = mempty
+  , _taxWithheldFHSS  = mempty
+  , _taxableProfessionalIncome = mempty
+  }
+
+otherIncomeCategory1 :: Lens' (OtherIncome a) [(String, Money a)]
+otherIncomeCategory1 = lens _category1 (\s b -> s { _category1 = b })
+
+otherIncomeCategory2 :: Lens' (OtherIncome a) [(String, Money a)]
+otherIncomeCategory2 = lens _category2 (\s b -> s { _category2 = b })
+
+otherIncomeCategory3 :: Lens' (OtherIncome a) (Money a)
+otherIncomeCategory3 = lens _category3 (\s b -> s { _category3 = b })
+
+otherIncomeCategory4 :: Lens' (OtherIncome a) [(String, Money a)]
+otherIncomeCategory4 = lens _category4 (\s b -> s { _category4 = b })
+
+taxWithheldLumpSumPaymentsInArrears :: Lens' (OtherIncome a) (Money a)
+taxWithheldLumpSumPaymentsInArrears =
+  lens _taxWithheldLSPIA (\s b -> s { _taxWithheldLSPIA = b })
+
+taxWithheldAssessableFHSSReleasedAmount :: Lens' (OtherIncome a) (Money a)
+taxWithheldAssessableFHSSReleasedAmount =
+  lens _taxWithheldFHSS (\s b -> s { _taxWithheldFHSS = b })
+
+-- | __Z__ Taxable professional income
+--
+-- This field is used for __income averaging__ for /special professionals/
+-- (which this library does not yet implement).
+--
+taxableProfessionalIncome :: Lens' (OtherIncome a) (Money a)
+taxableProfessionalIncome =
+  lens _taxableProfessionalIncome (\s b -> s { _taxableProfessionalIncome = b })
+
+instance (Num a) => HasTaxableIncome OtherIncome a a where
+  taxableIncome = to $ \s ->
+    view (otherIncomeCategory1 . traverse . _2) s
+    <> view (otherIncomeCategory2 . traverse . _2) s
+    <> view otherIncomeCategory3 s
+    <> view (otherIncomeCategory4 . traverse . _2) s
+
+instance (Num a) => HasTaxWithheld OtherIncome a a where
+  taxWithheld = to $ \s ->
+    view taxWithheldLumpSumPaymentsInArrears s
+    <> view taxWithheldAssessableFHSSReleasedAmount s
diff --git a/src/Data/Tax/ATO/BPI.hs b/src/Data/Tax/ATO/BPI.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Tax/ATO/BPI.hs
@@ -0,0 +1,131 @@
+-- This file is part of hs-tax-ato
+-- Copyright (C) 2026  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/>.
+
+{-|
+
+Business and professional items (BPI) schedule
+
+Only Personal services income (PSI) and deductions are implemented at the
+current time.
+
+-}
+
+module Data.Tax.ATO.BPI
+  ( BusinessAndProfessionalItemsSchedule
+  , newBusinessAndProfessionalItemsSchedule
+  , personalServicesIncome
+
+  , PersonalServicesIncome
+  , newPersonalServicesIncome
+  , psiVoluntaryAgreement
+  , psiWhereABNNotQuoted
+  , psiLabourHireOrOtherSpecifiedPayments
+  , psiOther
+  , psiDeductionsForPaymentsToAssociatesForPrincipalWork
+  , psiTotalAmountOfOtherDeductions
+  , netPSI
+  ) where
+
+import Control.Lens
+import Data.Tax
+
+import Data.Tax.ATO.Common
+
+-- | Business and professional items (BPI) schedule
+data BusinessAndProfessionalItemsSchedule a = BusinessAndProfessionalItemsSchedule
+  { _personalServicesIncome :: PersonalServicesIncome a
+  }
+
+newBusinessAndProfessionalItemsSchedule :: (Num a) => BusinessAndProfessionalItemsSchedule a
+newBusinessAndProfessionalItemsSchedule = BusinessAndProfessionalItemsSchedule
+  { _personalServicesIncome = newPersonalServicesIncome
+  }
+
+instance (Num a, Ord a)
+    => HasTaxableIncome BusinessAndProfessionalItemsSchedule a a where
+  taxableIncome = personalServicesIncome . taxableIncome
+
+-- | __P1__ Personal services income (PSI)
+personalServicesIncome
+  :: Lens' (BusinessAndProfessionalItemsSchedule a) (PersonalServicesIncome a)
+personalServicesIncome =
+  lens _personalServicesIncome (\s b -> s { _personalServicesIncome = b })
+
+
+data PersonalServicesIncome a = PersonalServicesIncome
+  { _psiVoluntaryAgreement :: Money a
+  , _psiWhereABNNotQuoted :: Money a
+  , _psiLabourHireOrOtherSpecifiedPayments :: Money a
+  , _psiOther :: Money a
+  , _psiDeductionsForPaymentsToAssociatesForPrincipalWork :: Money a
+  , _psiTotalAmountOfOtherDeductions :: Money a
+  }
+
+newPersonalServicesIncome :: (Num a) => PersonalServicesIncome a
+newPersonalServicesIncome = PersonalServicesIncome
+  { _psiVoluntaryAgreement = mempty
+  , _psiWhereABNNotQuoted = mempty
+  , _psiLabourHireOrOtherSpecifiedPayments = mempty
+  , _psiOther = mempty
+  , _psiDeductionsForPaymentsToAssociatesForPrincipalWork = mempty
+  , _psiTotalAmountOfOtherDeductions = mempty
+  }
+
+-- | __M__ PSI — voluntary agreement
+psiVoluntaryAgreement :: Lens' (PersonalServicesIncome a) (Money a)
+psiVoluntaryAgreement =
+  lens _psiVoluntaryAgreement (\s b -> s { _psiVoluntaryAgreement = b })
+
+-- | __N__ PSI — where Australian business number not quoted
+psiWhereABNNotQuoted :: Lens' (PersonalServicesIncome a) (Money a)
+psiWhereABNNotQuoted =
+  lens _psiWhereABNNotQuoted (\s b -> s { _psiWhereABNNotQuoted = b })
+
+-- | __O__ PSI — labour hire or other specified payments
+psiLabourHireOrOtherSpecifiedPayments :: Lens' (PersonalServicesIncome a) (Money a)
+psiLabourHireOrOtherSpecifiedPayments =
+  lens _psiLabourHireOrOtherSpecifiedPayments (\s b -> s { _psiLabourHireOrOtherSpecifiedPayments = b })
+
+-- | __J__ PSI — other
+psiOther :: Lens' (PersonalServicesIncome a) (Money a)
+psiOther =
+  lens _psiOther (\s b -> s { _psiOther = b })
+
+-- | __K__ Deductions for payments to associates for principal work
+psiDeductionsForPaymentsToAssociatesForPrincipalWork
+  :: Lens' (PersonalServicesIncome a) (Money a)
+psiDeductionsForPaymentsToAssociatesForPrincipalWork =
+  lens _psiDeductionsForPaymentsToAssociatesForPrincipalWork
+    (\s b -> s { _psiDeductionsForPaymentsToAssociatesForPrincipalWork = b })
+
+-- | __L__ Total amount of other deductions against PSI
+psiTotalAmountOfOtherDeductions :: Lens' (PersonalServicesIncome a) (Money a)
+psiTotalAmountOfOtherDeductions =
+  lens _psiTotalAmountOfOtherDeductions (\s b -> s { _psiTotalAmountOfOtherDeductions = b })
+
+
+instance (Num a, Ord a) => HasTaxableIncome PersonalServicesIncome a a where
+  taxableIncome = to (max mempty . netPSI)
+
+-- | Net PSI
+netPSI :: (Num a) => PersonalServicesIncome a -> Money a
+netPSI a =
+  view psiVoluntaryAgreement a
+  $+$ view psiWhereABNNotQuoted a
+  $+$ view psiLabourHireOrOtherSpecifiedPayments a
+  $+$ view psiOther a
+  $-$ view psiDeductionsForPaymentsToAssociatesForPrincipalWork a
+  $-$ view psiTotalAmountOfOtherDeductions a
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
@@ -15,6 +15,8 @@
 -- along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
+{-# LANGUAGE LambdaCase #-}
+
 {-|
 
 Types and calculations for /capital gains tax/ (CGT).
@@ -25,24 +27,83 @@
 
 The main function you need is 'assessCGTEvents'.
 
--}
+Things that are __not yet implemented__:
 
+* Small business CGT concessions
+* Exemptions, rollovers, and earnout arrangements
+
+-}
 module Data.Tax.ATO.CGT
   (
+  -- * CGT assets
+    CGTAssetType(..)
+  , CGTAssetFlag(..)
+  , CGTAsset
+  , assetDescription
+  , assetShare
+  , assetType
+  , assetFlag
+  , assetUnits
+  , assetAcquisitionDate
+  , assetAcquisitionPrice
+  , assetAcquisitionCosts
+  , assetCapitalCosts
+  , assetOwnershipCosts
+  -- ** Constructors
+  , newCGTAsset
+
   -- * CGT events
-    CGTEvent(..)
+  , CGTEvent
+  , eventAsset
+  , eventCode
+  , eventDate
+  , eventPrice
+  , eventCosts
+  , CGTExemptionRolloverOrAdditionalDiscountCode(..)
+  -- ** Constructors
+  , cgtEventA1
+  , cgtEventB1
+  , cgtEventC1
+  , cgtEventC2
+  , cgtEventC3
+  , cgtEventE10
+  -- ** Helper functions
+  , eventProceeds
 
+  -- * Capital gains from trusts
+  , CapitalGainsFromTrust
+  , newCapitalGainsFromTrust
+  , trustGrossCapitalGain
+  , trustCapitalLossesApplied
+  , trustCGTDiscountApplied
+  , trustCGTSmallBusinessConcessionApplied
+  , prepTrustCGT
+
   -- * CGT assessments for tax returns
   , assessCGTEvents
   , CGTAssessment
   , nullCGTAssessment
-  , HasCapitalLossCarryForward(..)
   , cgtNetGain
   , cgtTotalCurrentYearGains
+  , cgtTotalCurrentYearLosses
+  , cgtTotalCurrentYearLossesApplied
+  , cgtTotalPriorYearLossesApplied
+  , cgtTotalCGTDiscountApplied
   , cgtNetLossesCarriedForward
+  , cgtGainsAndLossesByCategory
 
+  -- ** Carry-forward
+  , HasCapitalLossCarryForward(..)
+  , CapitalLossCarryForward
+  , newCapitalLossCarryForward
+  , capitalLossCarryForwardCollectables
+  , capitalLossCarryForwardOther
+
+  -- ** Helper types for CGT Schedule
+  , CGTScheduleCategory(..)
+
   -- * CGT computations
-  , HasCapitalGain(..)
+  , capitalGain
   , capitalLoss
   , isCapitalGain
   , isCapitalLoss
@@ -52,99 +113,389 @@
 import Data.Foldable (toList)
 import Data.List (partition)
 
-import Control.Lens (Getter, Lens', both, lens, over, to, view)
+import Control.Lens
+  ( Getter, Lens', Traversal'
+  , (&), _3, anyOf, both, lens, over, preview, set, to, view, views
+  )
+import qualified Data.Map as M
 import Data.Time.Calendar (Day, diffDays)
 import Data.Tax
 
--- | A CGT Event (usually an asset disposal)
+import Data.Tax.ATO.Common (EntityType(..))
+
+
+-- | The CGT schedule requires reporting of gains and losses for
+-- different asset types.  Also, collectables and personal use
+-- assets are subject to different rules from other assets.
+-- Specifically, capital losses from personal use assets are
+-- disregarded, and capital losses from collectables can only offset
+-- capital gains from collectables.
 --
-data CGTEvent a = CGTEvent
-  { assetDesc :: String
-  , units :: a
-  , acquisitionDate :: Day
-  , acquisitionPrice :: Money a
-  , acquisitionCosts :: Money a
-  , disposalDate :: Day
-  , disposalPrice :: Money a
-  , disposalCosts :: Money a
-  , capitalCosts :: Money a
-  , ownershipCosts :: Money a
+data CGTAssetType
+  = AssetTypeSharesAUListed
+  | AssetTypeSharesOther
+  | AssetTypeUnitsAUListed
+  | AssetTypeUnitsOther
+  | AssetTypeRealEstateAU
+  | AssetTypeRealEstateOther
+  | AssetTypeCollectable
+  | AssetTypePersonalUse
+  | AssetTypeOther
+  deriving (Eq, Ord, Show)
+
+-- | Flags for CGT assets that can affect CGT calculations.
+--
+data CGTAssetFlag
+  = MainResidence
+  | AffordableHousing
+  | SmallBusinessActiveAsset
+  | EarlyStageInnovationCompany
+  deriving (Eq, Ord, Show)
+
+data CGTAsset a = CGTAsset
+  { _assetDescription       :: String
+  , _assetShare             :: Rational     -- owner's share of asset
+  , _assetType              :: CGTAssetType
+  , _assetFlag              :: Maybe CGTAssetFlag
+  , _assetUnits             :: Rational -- number of units
+  , _assetAcquisitionDate   :: Day
+  , _assetAcquisitionPrice  :: Money a  -- acquisition price (per unit)
+  , _assetAcquisitionCosts  :: Money a  -- acquisition costs (of parcel)
+  , _assetCapitalCosts      :: Money a
+  , _assetOwnershipCosts    :: Money a
   }
+  deriving (Functor, Show)
+
+-- | Construct a new CGT asset.  Capital and ownership costs are set to zero.
+-- Use 'assetFlag' to set a flag, if relevant.
+newCGTAsset
+  :: (Num a)
+  => String   -- ^ asset description
+  -> Rational -- ^ owner's share (a fraction between 0 and (typically) 1)
+  -> CGTAssetType
+  -> Rational -- ^ quantity of units/shares
+  -> Day      -- ^ acquisition date
+  -> Money a  -- ^ price per unit/share
+  -> Money a  -- ^ acquisition costs (e.g. brokerage, settlement costs, etc)
+  -> CGTAsset a
+newCGTAsset desc share typ units date price cost =
+  CGTAsset desc share typ Nothing units date price cost mempty mempty
+
+assetDescription :: Lens' (CGTAsset a) String
+assetDescription =
+  lens _assetDescription (\s b -> s { _assetDescription = b })
+
+assetShare :: Lens' (CGTAsset a) Rational
+assetShare =
+  lens _assetShare (\s b -> s { _assetShare = b })
+
+assetType :: Lens' (CGTAsset a) CGTAssetType
+assetType =
+  lens _assetType (\s b -> s { _assetType = b })
+
+assetFlag :: Lens' (CGTAsset a) (Maybe CGTAssetFlag)
+assetFlag =
+  lens _assetFlag (\s b -> s { _assetFlag = b })
+
+assetUnits :: Lens' (CGTAsset a) Rational
+assetUnits =
+  lens _assetUnits (\s b -> s { _assetUnits = b })
+
+assetAcquisitionDate :: Lens' (CGTAsset a) Day
+assetAcquisitionDate =
+  lens _assetAcquisitionDate (\s b -> s { _assetAcquisitionDate = b })
+
+assetAcquisitionPrice :: Lens' (CGTAsset a) (Money a)
+assetAcquisitionPrice =
+  lens _assetAcquisitionPrice (\s b -> s { _assetAcquisitionPrice = b })
+
+assetAcquisitionCosts :: Lens' (CGTAsset a) (Money a)
+assetAcquisitionCosts =
+  lens _assetAcquisitionCosts (\s b -> s { _assetAcquisitionCosts = b })
+
+assetCapitalCosts :: Lens' (CGTAsset a) (Money a)
+assetCapitalCosts =
+  lens _assetCapitalCosts (\s b -> s { _assetCapitalCosts = b })
+
+assetOwnershipCosts :: Lens' (CGTAsset a) (Money a)
+assetOwnershipCosts =
+  lens _assetOwnershipCosts (\s b -> s { _assetOwnershipCosts = b })
+
+
+data CGTExemptionRolloverOrAdditionalDiscountCode
+  = {- A -} SmallBusinessActiveAssetReduction
+  | {- B -} SmallBusinessRetirementExemption
+  | {- C -} SmallBusinessRollover
+  | {- D -} SmallBusiness15YearExemption
+  | {- E -} ForeignResidentCGTExemption
+  | {- F -} ScripForScripRollover
+  | {- I -} MainResidenceExemption
+  | {- J -} CapitalGainsDisregardedAsAResultOfTheSaleOfAPreCGTAsset
+  | {- K -} DisposalOrCreationOfAssetsInAWhollyOwnedCompany
+  | {- L -} ReplacementAssetRollovers
+  | {- M -} ExchangeOfSharesOrUnits
+  | {- N -} ExchangeOfRightsOrOptions
+  | {- O -} ExchangeOfSharesInOneCompanyForSharesInAnotherCompany
+  | {- P -} ExchangeOfUnitsInAUnitTrustForSharesInACompany
+  | {- R -} DemergerRollover
+  | {- S -} SameAssetRollovers
+  | {- T -} SmallBusinessRestructureRollover
+  | {- U -} EarlyStageInvestor
+  | {- V -} VentureCapitalInvestment
+  | {- W -} AffordableHousingDiscount
+  | {- X -} OtherExemptionsAndRollovers
+  deriving (Eq, Ord, Show)
+
+data CGTEventType a
+  = CGTEventEnd (CGTAsset a)
+    -- ^ Ownership of a CGT asset ends (or contracted)
+  | CGTEventReAsset (CGTAsset a)
+    -- ^ A CGT event relating to the asset, where cost base ignored.
+    -- Acquisition date determines discounts, etc.
+  | CGTEventNoAsset
+    -- ^ A CGT event not relating to an owned asset
+  | CGTEventTrustDistribution IsDiscountable
+    -- ^ Special purpose constructor for representing distributions
+    -- of capital gains from trusts.
+  deriving (Functor, Show)
+
+data IsDiscountable = Discountable | NotDiscountable
   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)
+-- | A CGT event (usually an asset disposal)
+data CGTEvent a = CGTEvent
+  { _eventType  :: CGTEventType a
+  , _eventCode  :: Maybe CGTExemptionRolloverOrAdditionalDiscountCode
+  , _eventDate  :: Day
+  , _eventPrice :: Money a
+  , _eventCosts :: Money a
+  }
+  deriving (Functor, Show)
 
-reducedCostBase :: Num a => CGTEvent a -> Money a
-reducedCostBase event =
-  (units event *$ acquisitionPrice event)
-  $+$ acquisitionCosts event
-  $+$ disposalCosts event
-  $+$ capitalCosts event
+eventType :: Lens' (CGTEvent a) (CGTEventType a)
+eventType =
+  lens _eventType (\s b -> s { _eventType = b })
 
-costBase :: Num a => CGTEvent a -> Money a
-costBase event = reducedCostBase event $+$ ownershipCosts event
+-- | A CGT event could have zero or one related assets.
+eventAsset :: Traversal' (CGTEvent a) (CGTAsset a)
+eventAsset = eventType . l
+  where
+  l f = \case
+    CGTEventEnd a     -> CGTEventEnd      <$> f a
+    CGTEventReAsset a -> CGTEventReAsset  <$> f a
+    s                 -> pure s
 
-capitalGain' :: (Num a, Ord a) => CGTEvent a -> Money a
-capitalGain' event =
-  max mempty (units event *$ disposalPrice event $-$ costBase event)
+eventCode :: Lens' (CGTEvent a) (Maybe CGTExemptionRolloverOrAdditionalDiscountCode)
+eventCode =
+  lens _eventCode (\s b -> s { _eventCode = b })
 
--- | The capital loss as a /non-negative/ amount.
--- /$0/ if the event is not a loss.
+eventDate :: Lens' (CGTEvent a) Day
+eventDate =
+  lens _eventDate (\s b -> s { _eventDate = b })
+
+eventPrice :: Lens' (CGTEvent a) (Money a)
+eventPrice =
+  lens _eventPrice (\s b -> s { _eventPrice = b })
+
+eventCosts :: Lens' (CGTEvent a) (Money a)
+eventCosts =
+  lens _eventCosts (\s b -> s { _eventCosts = b })
+
+-- | A1 - Disposal of a CGT asset
+cgtEventA1
+  :: CGTAsset a
+  -- ^ The asset the event is associated with.  If disposing of a
+  -- share or fewer shares/units than held, the asset must be split
+  -- beforehand.
+  -> Maybe CGTExemptionRolloverOrAdditionalDiscountCode
+  -- ^ Exemption, rollover or discount code applying to this event, if any.
+  -> Day      -- ^ When the disposal contract is entered into or, if none,
+              --   when the entity stops being the asset's owner
+  -> Money a  -- ^ Capital proceeds (per unit)
+  -> Money a  -- ^ Costs of disposal (consolidated; for whole asset)
+  -> CGTEvent a
+cgtEventA1 asset code date price cost =
+  CGTEvent (CGTEventEnd asset) code date price cost
+
+-- | B1 - Use and enjoyment before title passes
+cgtEventB1
+  :: CGTAsset a
+  -> Maybe CGTExemptionRolloverOrAdditionalDiscountCode
+  -- ^ Exemption, rollover or discount code applying to this event, if any.
+  -> Day      -- ^ When use of the CGT asset passes
+  -> Money a  -- ^ Capital proceeds (per unit)
+  -> Money a  -- ^ Costs of arrangement
+  -> CGTEvent a
+cgtEventB1 = cgtEventA1
+
+-- | C1 - Loss or destruction of a CGT asset
+cgtEventC1
+  :: CGTAsset a
+  -> Maybe CGTExemptionRolloverOrAdditionalDiscountCode
+  -- ^ Exemption, rollover or discount code applying to this event, if any.
+  -> Day      -- ^ When compensation is first received or, if none,
+              --   when the loss is discovered or destruction occurred
+  -> Money a  -- ^ Capital proceeds (per unit)
+  -> Money a  -- ^ Incidental costs relating to the event
+  -> CGTEvent a
+cgtEventC1 = cgtEventA1
+
+-- | C2 - Cancellation, surrender and similar endings
+cgtEventC2
+  :: CGTAsset a
+  -> Maybe CGTExemptionRolloverOrAdditionalDiscountCode
+  -- ^ Exemption, rollover or discount code applying to this event, if any.
+  -> Day      -- ^ When the contract ending an asset is entered into or,
+              --   if none, when an asset ends
+  -> Money a  -- ^ Capital proceeds (per unit)
+  -> Money a  -- ^ Costs relating to the event
+  -> CGTEvent a
+cgtEventC2 = cgtEventA1
+
+-- | C3 - End of an option to acquire shares etc
+cgtEventC3
+  :: (Num a)
+  => CGTAsset a
+  -- ^ The option.  Its acqusition price could be /negative/, i.e.
+  -- capital proceeds from granting the option.  Acquisition costs
+  -- record the expenditure in granting the option.
+  -- granting the option is recorded in the a
+  -> Day      -- ^ When the option ends
+  -> CGTEvent a
+cgtEventC3 asset date = CGTEvent (CGTEventEnd asset) Nothing date (Money 0) (Money 0)
+
+-- | E10 - Annual cost base reduction exceeds cost base of interest
+-- in attribution managed investment trust.
+cgtEventE10
+  :: (Num a)
+  => CGTAsset a
+  -- ^ The asset the event is associated with.
+  -> Day
+  -> Money a
+  -- ^ Amount cost base reduction exceeds cost base (consolidated; for whole asset)
+  -> CGTEvent a
+cgtEventE10 asset date amount =
+  CGTEvent (CGTEventReAsset asset) Nothing date amount (Money 0)
+
+-- | Capital proceeds arising from CGT event
+eventProceeds :: CGTEvent Rational -> Money Rational
+eventProceeds ev = case preview (eventAsset . assetUnits) ev of
+  Just n  -> n *$ view eventPrice ev  -- asset with units (possibly 1)
+  Nothing ->      view eventPrice ev  -- bare capital proceeds
+
+
+-- | @sub x y@ = subtract @y@ from @x@, clamping to 0 and
+-- returning @(result, leftovers)@
 --
-capitalLoss :: (Num a, Ord a) => CGTEvent a -> Money a
-capitalLoss event = over money abs $
-  min mempty (units event *$ disposalPrice event $-$ reducedCostBase event)
+sub :: (Num a, Ord a) => Money a -> Money a -> (Money a, Money a)
+sub x y =
+  let r = x $-$ y
+  in (max mempty r, over money abs (min mempty r))
 
+
+-- | Reduced cost base of the (whole) asset subject to this CGT event
+assetReducedCostBase :: (Fractional a) => CGTAsset a -> Money a
+assetReducedCostBase asset =
+  ( views assetUnits fromRational asset
+    *$ view assetAcquisitionPrice asset )
+  $+$ view assetAcquisitionCosts asset
+  $+$ view assetCapitalCosts asset
+
+-- | Cost base of the (whole) asset subject to this CGT event
+assetCostBase :: (Fractional a) => CGTAsset a -> Money a
+assetCostBase asset =
+  assetReducedCostBase asset
+  $+$ view assetOwnershipCosts asset
+
+-- | Take of the amount in the same proportion as the share of the given asset.
+shareOf :: (Fractional a) => CGTAsset a -> Money a -> Money a
+shareOf asset = ($* views assetShare fromRational asset)
+
+-- | Capital gain, scaled to the owner's share (per 'assetShare').
+-- __Zero__ if there is no capital gain.  Does not apply discounts
+-- or exemptions.
+capitalGain :: (Fractional a, Ord a) => CGTEvent a -> Money a
+capitalGain event =
+  max mempty
+  $ case view eventType event of
+    CGTEventEnd asset ->
+      shareOf asset
+      $ views assetUnits fromRational asset *$ view eventPrice event
+        $-$ assetCostBase asset
+        $-$ view eventCosts event
+    CGTEventReAsset asset ->
+      shareOf asset
+      $ view eventPrice event
+        $-$ view eventCosts event
+    CGTEventNoAsset ->
+      view eventPrice event
+      $-$ view eventCosts event
+    CGTEventTrustDistribution _ ->
+      view eventPrice event
+
+-- | The capital loss as a __non-negative__ amount, scaled to the
+-- owner's share (per 'assetShare').  __Zero__ if there is no
+-- capital loss.
+--
+-- Capital losses on personal use assets are disregarded;
+-- if type is 'AssetTypePersonalUse' this will always be zero.
+--
+capitalLoss :: (Fractional a, Ord a) => CGTEvent a -> Money a
+capitalLoss event =
+  fmap abs . min mempty
+  $ case view eventType event of
+    CGTEventEnd asset ->
+      shareOf asset
+      $ ( case view assetType asset of AssetTypePersonalUse -> const (Money 0) ; _ -> id )
+      $ views assetUnits fromRational asset *$ view eventPrice event
+        $-$ assetReducedCostBase asset
+        $-$ view eventCosts event
+    CGTEventReAsset asset ->
+      shareOf asset
+      $ view eventPrice event
+        $-$ view eventCosts event
+    CGTEventNoAsset ->
+      view eventPrice event
+      $-$ view eventCosts event
+    CGTEventTrustDistribution _ ->
+      mempty  -- trust cannot distribute a capital loss
+
+
 -- | Whether the CGT event is a capital gain.  /Not the opposite
 -- of 'isCapitalLoss'!/  A CGT event may be neither a loss nor a
 -- gain.
 --
-isCapitalGain :: (Num a, Ord a) => CGTEvent a -> Bool
-isCapitalGain = (> mempty) . capitalGain'
+isCapitalGain :: (Fractional a, Ord a) => CGTEvent a -> Bool
+isCapitalGain = (> mempty) . capitalGain
 
 -- | Whether the CGT event is a capital loss.  /Not the opposite
 -- of 'isCapitalGain'!/  A CGT event may be neither a loss nor a
 -- gain.
 --
-isCapitalLoss :: (Num a, Ord a) => CGTEvent a -> Bool
+isCapitalLoss :: (Fractional a, Ord a) => CGTEvent a -> Bool
 isCapitalLoss = (> mempty) . capitalLoss
 
--- | Whether the 50% CGT discount is applicable to this event (only
--- with regard to duration of holding; acquisition date ignored).
+-- | Whether the CGT discount is (potentially) applicable to this event.
+-- Whether it is actually applicable and the discount proportion depends
+-- on the type of the entity being assessed.
 --
 discountApplicable :: CGTEvent a -> Bool
 discountApplicable ev =
-  diffDays (disposalDate ev) (acquisitionDate ev) > 365
+  case view eventType ev of
+    CGTEventTrustDistribution    Discountable -> True
+    CGTEventTrustDistribution NotDiscountable -> False
+    _ -> case preview eventAsset ev of
+      Nothing -> False
+      Just a  -> diffDays (view eventDate ev) (view assetAcquisitionDate a) > 365
 
--- | Types that may have a capital gain.  Non-discounted, losses ignored.
-class HasCapitalGain a b c where
-  capitalGain :: Getter (a b) (Money c)
 
--- | Capital gain as a positive amount.  /$0/ if the event not a gain.
-instance (Num a, Ord a) => HasCapitalGain CGTEvent a a where
-  capitalGain = to capitalGain'
-
--- | Sum of capital gains, ignoring losses.
--- Input __H__ at /item 18/ on tax return.
+-- | Assess the total capital gains and net capital gain or loss.
 --
-instance (Foldable t, HasCapitalGain x a a, Num a) => HasCapitalGain t (x a) a where
-  capitalGain = to (foldMap (view capitalGain))
-
-
--- | @sub x y@ = subtract @y@ from @x@, clamping to 0 and
--- returning @(result, leftovers)@
+-- __Assumes all events relate to a particular financial year.__
+-- This function does not check or filter the input data.
 --
-sub :: (Num a, Ord a) => Money a -> Money a -> (Money a, Money a)
-sub x y =
-  let r = x $-$ y
-  in (max mempty r, over money abs (min mempty r))
-
--- | Assess the total capital gains and net capital gain or loss.
+-- Use 'prepTrustCGT' to process 'CapitalGainsFromTrust' values into
+-- CGT events.
 --
 -- Losses are used to offset non-discountable capital gains
 -- first, then discountable gains, before the discount is applied
@@ -154,59 +505,230 @@
 --
 assessCGTEvents
   :: (Fractional a, Ord a, Foldable t)
-  => Money a            -- ^ capital loss carried forward
+  => EntityType
+  -> CapitalLossCarryForward a
   -> t (CGTEvent a)
   -> CGTAssessment a
-assessCGTEvents carry events =
+assessCGTEvents etype carry events =
   let
     l = toList events
+    (lC, lO) = partition (anyOf (eventAsset . assetType) (== AssetTypeCollectable)) l
+    carryC = view capitalLossCarryForwardCollectables carry
+    carryO = view capitalLossCarryForwardOther        carry
+    r@(rC, rO) =
+      ( applyLossesAndDiscounts etype carryC lC
+      , applyLossesAndDiscounts etype carryO lO
+      )
+    appliedLossC        = view vectorTotalLoss rC $+$ carryC $-$ view vectorUnappliedLoss rC
+    appliedLossO        = view vectorTotalLoss rO $+$ carryO $-$ view vectorUnappliedLoss rO
+    curFYLossesAppliedC = min appliedLossC (view vectorTotalLoss rC)
+    curFYLossesAppliedO = min appliedLossO (view vectorTotalLoss rO)
+    priorLossesAppliedC = appliedLossC $-$ curFYLossesAppliedC
+    priorLossesAppliedO = appliedLossO $-$ curFYLossesAppliedO
+    byCategory =
+      M.fromListWith (<>) $ flip fmap l $ \ev ->
+        ( scheduleCategory ev, (capitalGain ev, capitalLoss ev) )
+  in
+    CGTAssessment
+      (view (both . vectorTotalGain) r)
+      (view (both . vectorTotalLoss) r)
+
+      -- current year losses applied
+      ( curFYLossesAppliedC <> curFYLossesAppliedO )
+
+      -- prior year losses applied
+      ( priorLossesAppliedC <> priorLossesAppliedO )
+
+      ( newCapitalLossCarryForward
+        & set capitalLossCarryForwardCollectables (view vectorUnappliedLoss rC)
+        & set capitalLossCarryForwardOther        (view vectorUnappliedLoss rO) )
+      (view (both . vectorDiscount) r)
+      (view (both . vectorNetGain) r)
+
+      byCategory
+
+-- | Internal data type for asset type-partitioned CGT calculations
+data CGTVector a = CGTVector
+  { _vectorTotalGain :: Money a
+  , _vectorTotalLoss :: Money a
+  , _vectorNetGain   :: Money a
+  , _vectorUnappliedLoss :: Money a
+  , _vectorDiscount  :: Money a
+  }
+vectorTotalGain, vectorTotalLoss, vectorNetGain, vectorUnappliedLoss, vectorDiscount
+  :: Lens' (CGTVector a) (Money a)
+vectorTotalGain     = lens _vectorTotalGain     (\s b -> s { _vectorTotalGain = b })
+vectorTotalLoss     = lens _vectorTotalLoss     (\s b -> s { _vectorTotalLoss = b })
+vectorNetGain       = lens _vectorNetGain       (\s b -> s { _vectorNetGain = b })
+vectorUnappliedLoss = lens _vectorUnappliedLoss (\s b -> s { _vectorUnappliedLoss = b })
+vectorDiscount      = lens _vectorDiscount      (\s b -> s { _vectorDiscount = b })
+
+-- | Amount of capital gains from a trust or trusts (including managed funds).
+--
+-- Use 'newCapitalGainsFromTrust' to construct.  Apply 'prepTrustCGT' to
+-- produce CGT events representing the grossed-up capital gains.
+--
+-- The following optics give access to the fields (common use cases should
+-- not need them):
+--
+-- * 'trustGrossCapitalGain'
+-- * 'trustCapitalLossesApplied'
+-- * 'trustCGTDiscountApplied'
+-- * 'trustCGTSmallBusinessConcessionApplied'
+--
+data CapitalGainsFromTrust a = CapitalGainsFromTrust
+  { _trustGrossGain   :: Money a
+  , _trustLossesAppl  :: Money a
+  , _trustCGTDiscAppl :: Money a
+  , _trustSBConcAppl  :: Money a
+  }
+  deriving (Show)
+
+-- Construct a 'CapitalGainsFromTrust'.
+--
+-- Values are accepted without checking.  It is possible to
+-- construct a value that doesn't make sense (e.g. a CGT
+-- discount that is more than half of the gross gain).
+--
+newCapitalGainsFromTrust
+  :: Money a  -- ^ Gross capital gain
+  -> Money a  -- ^ Capital losses applied
+  -> Money a  -- ^ CGT discount applied
+  -> Money a  -- ^ CGT small business concessions applied
+  -> CapitalGainsFromTrust a
+newCapitalGainsFromTrust = CapitalGainsFromTrust
+
+trustGrossCapitalGain                   :: Lens' (CapitalGainsFromTrust a) (Money a)
+trustGrossCapitalGain
+  = lens _trustGrossGain (\s b -> s { _trustGrossGain = b })
+
+trustCapitalLossesApplied               :: Lens' (CapitalGainsFromTrust a) (Money a)
+trustCapitalLossesApplied
+  = lens _trustLossesAppl (\s b -> s { _trustLossesAppl = b })
+
+trustCGTDiscountApplied                 :: Lens' (CapitalGainsFromTrust a) (Money a)
+trustCGTDiscountApplied
+  = lens _trustCGTDiscAppl (\s b -> s { _trustCGTDiscAppl = b })
+
+trustCGTSmallBusinessConcessionApplied  :: Lens' (CapitalGainsFromTrust a) (Money a)
+trustCGTSmallBusinessConcessionApplied
+  = lens _trustSBConcAppl (\s b -> s { _trustSBConcAppl = b })
+
+
+-- | Produce CGT events representing grossed-up capital gains from
+-- the trust distribution.  Could produce multiple events, in cases where
+-- the CGT discount or small business concessions have been applied to
+-- only a proportion of the distributed gain.
+--
+prepTrustCGT :: (Fractional a, Eq a) => CapitalGainsFromTrust a -> [CGTEvent a]
+prepTrustCGT trust =
+  let
+    gross = view trustGrossCapitalGain trust
+    loss = view trustCapitalLossesApplied trust
+
+    discount = view trustCGTDiscountApplied trust
+    discountable = discount $* 2
+    nonDiscountable = gross $-$ loss $-$ discountable
+
+    sbc = view trustCGTSmallBusinessConcessionApplied trust
+    sbcEligible = sbc $* 2
+    sbcProportion = sbcEligible $/$ (gross $-$ loss $-$ discount)
+    sbcCode = Just SmallBusinessActiveAssetReduction
+
+    someDay = toEnum 0 :: Day  -- ghastly hack, = 1858-11-17
+    mk (t,c,x) = CGTEvent (CGTEventTrustDistribution t) c someDay x mempty
+  in
+    fmap mk . filter ((/=) mempty . view _3) $
+      [ ( NotDiscountable, sbcCode, nonDiscountable $* sbcProportion )
+      , ( NotDiscountable, Nothing, nonDiscountable $* (1 - sbcProportion) )
+      , (    Discountable, sbcCode,    discountable $* sbcProportion )
+      , (    Discountable, Nothing,    discountable $* (1 - sbcProportion) )
+      ]
+
+
+applyLossesAndDiscounts
+  :: (Fractional a, Ord a)
+  => EntityType
+  -> Money a    -- ^ unapplied losses from previous income years
+  -> [CGTEvent a]
+  -> CGTVector a
+applyLossesAndDiscounts etype carry l =
+  let
     (discountableGain, nonDiscountableGain) =
-      over both (view capitalGain) (partition discountApplicable l)
+      over both (foldMap 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
+    discount = discountableGainLossesApplied $* discountProportion etype
     discountedGain = nonDiscountableGainLossesApplied <> (discountableGainLossesApplied $-$ discount)
-
-    lossesApplied = (totalLoss <> carry) $-$ finalUnappliedLosses
-    priorYearLossesApplied = min carry lossesApplied
-    currentYearLossesApplied = lossesApplied $-$ priorYearLossesApplied
   in
-    CGTAssessment
-      totalGain
-      totalLoss
-      currentYearLossesApplied
-      priorYearLossesApplied
-      finalUnappliedLosses
-      discount
-      discountedGain
+    CGTVector totalGain totalLoss discountedGain finalUnappliedLosses discount
 
--- | Total undiscounted gains and net gain/loss for tax assessment
+discountProportion :: (Fractional a) => EntityType -> a
+discountProportion etype = fromRational $ case etype of
+  Individual  -> 1 / 2
+  Trust       -> 1 / 2
+  SuperFund   -> 1 / 3
+  Company     -> 0
+
+
+--
+-- CGTAssessment data type
+--
+
+data CGTScheduleCategory
+  = CGTScheduleCategorySharesAUListed
+  | CGTScheduleCategorySharesOther
+  | CGTScheduleCategoryUnitsAUListed
+  | CGTScheduleCategoryUnitsOther
+  | CGTScheduleCategoryRealEstateAU
+  | CGTScheduleCategoryRealEstateOther
+  | CGTScheduleCategoryTrust
+  | CGTScheduleCategoryCollectable
+  | CGTScheduleCategoryOther
+  | CGTScheduleCategoryDeferred
+  deriving (Eq, Ord, Show)
+
+scheduleCategory :: CGTEvent a -> CGTScheduleCategory
+scheduleCategory ev =
+  case view eventType ev of
+    CGTEventTrustDistribution _
+      -> CGTScheduleCategoryTrust
+    _
+      -> case preview (eventAsset . assetType) ev of
+        Just AssetTypeSharesAUListed  -> CGTScheduleCategorySharesAUListed
+        Just AssetTypeSharesOther     -> CGTScheduleCategorySharesOther
+        Just AssetTypeUnitsAUListed   -> CGTScheduleCategoryUnitsAUListed
+        Just AssetTypeUnitsOther      -> CGTScheduleCategoryUnitsOther
+        Just AssetTypeRealEstateAU    -> CGTScheduleCategoryRealEstateAU
+        Just AssetTypeRealEstateOther -> CGTScheduleCategoryRealEstateOther
+        Just AssetTypeCollectable     -> CGTScheduleCategoryCollectable
+        Just AssetTypePersonalUse     -> CGTScheduleCategoryOther
+        Just AssetTypeOther           -> CGTScheduleCategoryOther
+        Nothing                       -> CGTScheduleCategoryOther
+    -- TODO transitional relief deferrals?
+
+
+-- | This data type stores the completed CGT computations for some
+-- financial year.  It includes all data required for the /CGT schedule/
+-- and the tax return CGT section.
+--
 data CGTAssessment a = CGTAssessment
   { _totalCurrentYearCapitalGains :: Money a
   , _totalCurrentYearCapitalLosses :: Money a
   , _totalCurrentYearCapitalLossesApplied :: Money a
   , _totalPriorYearCapitalLossesApplied :: Money a
-  , _netCapitalLossesCarriedForward :: Money a
+  , _capitalLossCarryForward :: CapitalLossCarryForward a
   , _totalCGTDiscountApplied :: Money a
   , _netCapitalGain :: Money a
+  , _byCategory :: M.Map CGTScheduleCategory (Money a, Money a)
   }
-  deriving (Eq)
+  deriving (Eq, Functor, Show)
 
 -- | A 'CGTAssessment' whose values are all zero
 nullCGTAssessment :: (Num a) => CGTAssessment a
-nullCGTAssessment = CGTAssessment mempty mempty mempty mempty mempty mempty mempty
-
-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)
+nullCGTAssessment = CGTAssessment mempty mempty mempty mempty mempty mempty mempty mempty
 
 -- | __18A__ The net capital gain, or zero if a loss.
 cgtNetGain :: Lens' (CGTAssessment a) (Money a)
@@ -217,16 +739,68 @@
 cgtTotalCurrentYearGains =
   lens _totalCurrentYearCapitalGains (\s b -> s { _totalCurrentYearCapitalGains = b })
 
+cgtTotalCurrentYearLosses :: Lens' (CGTAssessment a) (Money a)
+cgtTotalCurrentYearLosses =
+  lens _totalCurrentYearCapitalLosses (\s b -> s { _totalCurrentYearCapitalLosses = b })
+
+cgtTotalCurrentYearLossesApplied :: Lens' (CGTAssessment a) (Money a)
+cgtTotalCurrentYearLossesApplied =
+  lens _totalCurrentYearCapitalLossesApplied (\s b -> s { _totalCurrentYearCapitalLossesApplied = b })
+
+cgtTotalCGTDiscountApplied :: Lens' (CGTAssessment a) (Money a)
+cgtTotalCGTDiscountApplied =
+  lens _totalCGTDiscountApplied (\s b -> s { _totalCGTDiscountApplied = b })
+
+cgtTotalPriorYearLossesApplied :: Lens' (CGTAssessment a) (Money a)
+cgtTotalPriorYearLossesApplied =
+  lens _totalPriorYearCapitalLossesApplied (\s b -> s { _totalPriorYearCapitalLossesApplied = b })
+
 -- | __18V__ Net capital losses carried forward to later income years
-cgtNetLossesCarriedForward :: Lens' (CGTAssessment a) (Money a)
-cgtNetLossesCarriedForward =
-  lens _netCapitalLossesCarriedForward (\s b -> s { _netCapitalLossesCarriedForward = b })
+cgtNetLossesCarriedForward :: (Num a) => Getter (CGTAssessment a) (Money a)
+cgtNetLossesCarriedForward = to $ \o ->
+  view (capitalLossCarryForward . capitalLossCarryForwardCollectables) o
+  <> view (capitalLossCarryForward . capitalLossCarryForwardOther) o
 
+cgtGainsAndLossesByCategory :: Lens' (CGTAssessment a) (M.Map CGTScheduleCategory (Money a, Money a))
+cgtGainsAndLossesByCategory =
+  lens _byCategory (\s b -> s { _byCategory = b })
+
+
+data CapitalLossCarryForward a = CapitalLossCarryForward
+  { _carryC :: Money a -- losses on collectables
+  , _carryO :: Money a -- all other losses
+  }
+  deriving (Eq, Functor, Show)
+
+instance (Num a) => Semigroup (CapitalLossCarryForward a) where
+  CapitalLossCarryForward lc lo <> CapitalLossCarryForward rc ro =
+    CapitalLossCarryForward (lc <> rc) (lo <> ro)
+
+instance (Num a) => Monoid (CapitalLossCarryForward a) where
+  mempty = CapitalLossCarryForward mempty mempty
+
+
+-- | Create a new capital loss carry-forward object with
+-- all components (collectables and other) set to zero.
+--
+-- @
+-- newCapitalLossCarryForward = mempty
+-- @
+--
+newCapitalLossCarryForward :: (Num a) => CapitalLossCarryForward a
+newCapitalLossCarryForward = mempty
+
+capitalLossCarryForwardCollectables :: Lens' (CapitalLossCarryForward a) (Money a)
+capitalLossCarryForwardCollectables = lens _carryC (\s b -> s { _carryC = b })
+capitalLossCarryForwardOther :: Lens' (CapitalLossCarryForward a) (Money a)
+capitalLossCarryForwardOther = lens _carryO (\s b -> s { _carryO = 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)
+  capitalLossCarryForward :: Lens' (a b) (CapitalLossCarryForward b)
 
 instance HasCapitalLossCarryForward CGTAssessment a where
   capitalLossCarryForward =
-    lens _netCapitalLossesCarriedForward (\s b -> s { _netCapitalLossesCarriedForward = b })
+    lens _capitalLossCarryForward (\s b -> s { _capitalLossCarryForward = 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
@@ -26,8 +26,11 @@
 
 module Data.Tax.ATO.Common
   (
+  -- * Taxable entity types
+    EntityType(..)
+
   -- * Tax tables
-    TaxTables(..)
+  , TaxTables(..)
 
   -- * Classes
   , HasTaxableIncome(..)
@@ -69,6 +72,13 @@
 import Data.Tax.ATO.PrivateHealthInsuranceRebate
 import Data.Tax.ATO.FY
 
+
+-- | Some tax calculations depend on the type of entity being assessed.
+data EntityType = Individual | Company | Trust | SuperFund
+  deriving (Eq, Ord, Show)
+
+
+
 -- | A set of tax tables for a particular financial year
 data TaxTables y a = TaxTables
   { ttIndividualIncomeTax :: Tax (Money a) (Money a)
@@ -76,6 +86,8 @@
   , ttMedicareLevySurcharge :: Tax (Money a) (Money a)
   , ttHelp :: Tax (Money a) (Money a)
   , ttSfss :: Tax (Money a) (Money a)
+  -- ^ Ignored since FY2020 (uses consolidated rate from @ttHelp@).
+  -- Just set it to @ttHelp@ or @undefined@.
   , ttAdditional :: Tax (Money a) (Money a)
   -- ^ Additional taxes and offsets that apply at EOY
   , ttPHIRebateRates :: PrivateHealthInsuranceRebateRates a
@@ -183,9 +195,11 @@
 class HasCentsPerKilometreMethod y where
   centsPerKilometre :: Num a => a
 
-instance (2016 <= y, y <= 2025, FinancialYear y) => HasCentsPerKilometreMethod y where
+instance (2016 <= y, y <= 2027, FinancialYear y) => HasCentsPerKilometreMethod y where
   centsPerKilometre = case fromProxy (Proxy @y) of
-    y | y == 2025           -> 88
+    y | y == 2027           -> 88  -- TODO confirm
+      | y == 2026           -> 88
+      | y == 2025           -> 88
       | y == 2024           -> 85
       | y == 2023           -> 78
       | y > 2020, y <= 2022 -> 72
@@ -214,12 +228,14 @@
 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
+instance (2023 <= y, y <= 2027, FinancialYear y) => HasFixedRateMethod y where
+  fixedRateMethodCentsPerHour = case fromProxy (Proxy @y) of
+    y | y == 2023           -> 67
+      | y == 2024           -> 67
+      | y == 2025           -> 70
+      | y == 2026           -> 70
+      | y == 2027           -> 70  -- TODO confirm
+      | otherwise           ->  0  -- can't happen
 
 -- | The updated fixed rate method available from the 2022–23 income
 -- year.  It covers:
diff --git a/src/Data/Tax/ATO/FY/FY2025.hs b/src/Data/Tax/ATO/FY/FY2025.hs
--- a/src/Data/Tax/ATO/FY/FY2025.hs
+++ b/src/Data/Tax/ATO/FY/FY2025.hs
@@ -17,10 +17,6 @@
 {-# 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
@@ -79,8 +75,8 @@
   , (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.
+-- Rebate adjustment factor = 0.987
+-- https://www.health.gov.au/news/phi-circulars/phi-2125-private-health-insurance-rebate-adjustment-factor-effective-1-april-2025
 privateHealthInsuranceRebateRates
   :: (Fractional a) => PrivateHealthInsuranceRebateRates a
 privateHealthInsuranceRebateRates =
diff --git a/src/Data/Tax/ATO/FY/FY2026.hs b/src/Data/Tax/ATO/FY/FY2026.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Tax/ATO/FY/FY2026.hs
@@ -0,0 +1,84 @@
+-- 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 DataKinds #-}
+
+-- | Tax tables for 2025–26 financial year.
+--
+-- Apart from the usual Medicare levy and private health insurance
+-- rebate adjustments, study loan repayments changed to a marginal
+-- tax (previously a threshold tax).
+--
+module Data.Tax.ATO.FY.FY2026 (FY, fyProxy, tables) where
+
+import Data.Proxy
+import Data.Tax
+import Data.Tax.ATO.Common
+import Data.Tax.ATO.PrivateHealthInsuranceRebate
+import qualified Data.Tax.ATO.FY.FY2025 as FY2025
+
+type FY = 2026
+fyProxy :: Proxy FY
+fyProxy = Proxy
+
+help :: (Fractional a, Ord a) => Tax (Money a) (Money a)
+help =
+  lesserOf
+    ( flat 0.10 )
+    ( marginal'
+        [ ( 67_000, 0.15       )
+        , (125_000, 0.17 - 0.15)
+        ]
+    )
+
+medicare :: (Fractional a) => MedicareLevyRatesAndThresholds a
+medicare = MedicareLevyRatesAndThresholds
+  { medicareLevyRate                                  = 0.02
+  , medicareLevyThresholdIndividual                   = Money 28011
+  , medicareLevyThresholdIndividualSeniorAndPensioner = Money 44268
+  , medicareLevyThresholdFamily                       = Money 47238
+  , medicareLevyThresholdFamilySeniorAndPensioner     = Money 61623
+  , medicareLevyThresholdDependentChildIncrease       = Money  4338
+  }
+
+-- | Medicare levy surcharge thresholds for 2025–26
+medicareLevySurcharge :: (Fractional a, Ord a) => Tax (Money a) (Money a)
+medicareLevySurcharge =
+  threshold (Money 101000) 0.01
+  <> threshold (Money 118000) 0.0025
+  <> threshold (Money 158000) 0.0025
+
+-- Rebate adjustment factor = 0.993.
+-- https://www.health.gov.au/news/phi-circulars/phi-1226-private-health-insurance-rebate-adjustment-factor-effective-1-april-2026
+privateHealthInsuranceRebateRates
+  :: (Fractional a) => PrivateHealthInsuranceRebateRates a
+privateHealthInsuranceRebateRates =
+  [ (101000, (0.24288, 0.24118), (0.28337, 0.28139), (0.32385, 0.32158) )
+  , (118000, (0.16192, 0.16079), (0.20240, 0.20098), (0.24288, 0.24118) )
+  , (158000, (0.08095, 0.08095), (0.12143, 0.12058), (0.16192, 0.16079) )
+  ]
+
+tables :: (Ord a, Fractional a) => TaxTables FY a
+tables = TaxTables
+  (ttIndividualIncomeTax FY2025.tables)
+  medicare
+  medicareLevySurcharge
+  help
+  help
+
+  lowIncomeTaxOffset2021
+
+  privateHealthInsuranceRebateRates
diff --git a/src/Data/Tax/ATO/FY/FY2027.hs b/src/Data/Tax/ATO/FY/FY2027.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Tax/ATO/FY/FY2027.hs
@@ -0,0 +1,95 @@
+-- This file is part of hs-tax-ato
+-- Copyright (C) 2026  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 2026–27 financial year.
+--
+-- The 16% personal income tax bracket drops to 15%.
+--
+-- These data are NOT YET FINAL.  We await the federal
+-- budget to confirm Medicare Levy thresholds, and the PHI
+-- rebate adjustment factor (~1 April 2027), and there could
+-- be other changes.
+--
+module Data.Tax.ATO.FY.FY2027 (FY, fyProxy, tables) where
+
+import Data.Proxy
+import Data.Tax
+import Data.Tax.ATO.Common
+import Data.Tax.ATO.PrivateHealthInsuranceRebate
+
+type FY = 2027
+fyProxy :: Proxy FY
+fyProxy = Proxy
+
+-- | In 2026–27 the 16% rate dropped to 15%
+individualIncomeTax :: (Fractional a, Ord a) => Tax (Money a) (Money a)
+individualIncomeTax = marginal'
+  [ (18200, 0.15)
+  , (45000, 0.30 - 0.15)
+  , (135000, 0.37 - 0.30)
+  , (190000, 0.45 - 0.37) ]
+
+help :: (Fractional a, Ord a) => Tax (Money a) (Money a)
+help =
+  lesserOf
+    ( flat 0.10 )
+    ( marginal'
+        [ ( 69_528, 0.15       )  -- TODO confirm these numbers
+        , (129_717, 0.17 - 0.15)
+        ]
+    )
+
+-- TODO update (budget night)
+medicare :: (Fractional a) => MedicareLevyRatesAndThresholds a
+medicare = MedicareLevyRatesAndThresholds
+  { medicareLevyRate                                  = 0.02
+  , medicareLevyThresholdIndividual                   = Money 28011
+  , medicareLevyThresholdIndividualSeniorAndPensioner = Money 44268
+  , medicareLevyThresholdFamily                       = Money 47238
+  , medicareLevyThresholdFamilySeniorAndPensioner     = Money 61623
+  , medicareLevyThresholdDependentChildIncrease       = Money  4338
+  }
+
+-- | Medicare levy surcharge thresholds for 2025–26
+-- TODO update
+medicareLevySurcharge :: (Fractional a, Ord a) => Tax (Money a) (Money a)
+medicareLevySurcharge =
+  threshold (Money 101000) 0.01
+  <> threshold (Money 118000) 0.0025
+  <> threshold (Money 158000) 0.0025
+
+-- TODO update (~April 1 PHI circular)
+privateHealthInsuranceRebateRates
+  :: (Fractional a) => PrivateHealthInsuranceRebateRates a
+privateHealthInsuranceRebateRates =
+  [ (101000, (0.24288, 0.24118), (0.28337, 0.28139), (0.32385, 0.32158) )
+  , (118000, (0.16192, 0.16079), (0.20240, 0.20098), (0.24288, 0.24118) )
+  , (158000, (0.08095, 0.08095), (0.12143, 0.12058), (0.16192, 0.16079) )
+  ]
+
+tables :: (Ord a, Fractional a) => TaxTables FY a
+tables = TaxTables
+  individualIncomeTax
+  medicare
+  medicareLevySurcharge
+  help
+  help
+
+  lowIncomeTaxOffset2021
+
+  privateHealthInsuranceRebateRates
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
@@ -26,16 +26,21 @@
 
 module Data.Tax.ATO.Pretty
   ( summariseTaxReturnInfo
+  , summariseCGTSchedule
   , summariseAssessment
   , formatMoney
   ) where
 
 import Data.List.NonEmpty as NE (NonEmpty, groupAllWith, head)
 
-import Control.Lens (ALens', cloneLens, foldOf, view, views)
+import Control.Lens
+  ( ALens'
+  , _2, allOf, at, both, cloneLens, foldOf, nullOf, view, views
+  )
 import qualified Text.PrettyPrint as P
 
 import Data.Tax.ATO
+import Data.Tax.ATO.Common (EntityType(..))
 import Data.Tax.ATO.CGT
 
 
@@ -58,20 +63,48 @@
     putCommas (a:b:c:d:rest) | d /= '-' = a:b:c:',':putCommas (d:rest)
     putCommas rest                      = rest
 
+omitIfZero :: ((a, Money Rational) -> P.Doc) -> (a, Money Rational) -> P.Doc
+omitIfZero = omitIf ((== mempty) . snd)
+
+omitIf :: (a -> Bool) -> (a -> P.Doc) -> a -> P.Doc
+omitIf test f a
+  | test a    = P.empty
+  | otherwise = f a
+
+data Formatter a = Formatter Int (a -> P.Doc)
+
+prepend :: String -> Formatter a -> Formatter a
+prepend s (Formatter w f) = Formatter (w + length s) ((P.text s <>) . f)
+
+append :: String -> Formatter a -> Formatter a
+append s (Formatter w f) = Formatter (w + length s) ((<> P.text s) . f)
+
+moneyFormatter :: Formatter (Money Rational)
+moneyFormatter = Formatter colWidthMoney formatMoney
+
+-- | Make a formatter that has a same width as the given formatter,
+-- but outputs blank space.
+blank :: Formatter a -> Formatter a
+blank (Formatter w _) = Formatter w (\_ -> P.text $ replicate w ' ')
+
+
 twoCol :: (P.Doc, Money Rational) -> P.Doc
-twoCol (label, value) = label P.$$ P.nest (80 - colWidthMoney) (formatMoney value)
+twoCol = twoCol' id moneyFormatter
 
-omitIfZero :: ((a, Money Rational) -> P.Doc) -> (a, Money Rational) -> P.Doc
-omitIfZero f rec@(_,x)
-  | x == mempty = P.empty
-  | otherwise   = f rec
+twoCol' :: (a -> P.Doc) -> Formatter b -> (a, b) -> P.Doc
+twoCol' fa (Formatter wb fb) (a, b) =
+  fa a
+  P.$$ P.nest (80 - wb) (fb b)
 
 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
+threeCol = threeCol' id moneyFormatter moneyFormatter
 
+threeCol' :: (a -> P.Doc) -> Formatter b -> Formatter c -> (a, b, c) -> P.Doc
+threeCol' fa (Formatter wb fb) (Formatter wc fc) (a, b, c) =
+  fa a
+  P.$$ P.nest (80 - wb - wc) (fb b)
+  P.<> fc c
+
 -- | 3-column layout with rightmost column blank
 threeColLeft :: (P.Doc, Money Rational) -> P.Doc
 threeColLeft (label, v1) =
@@ -84,27 +117,31 @@
 summariseTaxReturnInfo :: TaxReturnInfo y Rational -> P.Doc
 summariseTaxReturnInfo info =
   "Income"
-  P.$+$ vcatWith threeCol
+  P.$+$ vcatWith (omitIf (allOf both (== mempty)) threeCol)
     [ ( "  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.$+$ ( if nullOf dividends info then P.empty else "  11  Dividends" )
   P.$+$ views dividends summariseDividends info
   P.$+$ views ess summariseESS info
   P.$+$ summariseCGT info
-  P.$+$ vcatWith twoCol
+  P.$+$ vcatWith (omitIfZero twoCol)
     [ ("  20M Other net foreign source income" , view foreignIncome info)
     ]
+  P.$+$ views otherIncome summariseOtherIncome info
+  P.$+$ views businessAndProfessionalItems summariseBPI 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)
-    ]
 
+  P.$+$ ( \doc -> if P.isEmpty doc then P.empty else "Tax offsets" P.$+$ doc )
+    ( vcatWith (omitIfZero threeColLeft)
+        [ ("  20O Foreign income tax offset"  , view (offsets . foreignTaxOffset) info)
+        ]
+    )
+
 summariseDividends :: [Dividend Rational] -> P.Doc
 summariseDividends =
   vcatWith (threeCol . prep)
@@ -122,23 +159,132 @@
 summariseESS l =
   "  12  Employee share schemes"
   P.$+$ P.vcat
-    [ twoCol
+    [ omitIfZero twoCol
         ( "    D Discount from upfront schemes - eligible for reduction"
         , foldOf (traverse . essTaxedUpfrontReduction) l )
-    , twoCol
+    , omitIfZero twoCol
         ( "    E Discount from upfront schemes - ineligible for reduction"
         , foldOf (traverse . essTaxedUpfrontNoReduction) l )
-    , twoCol
+    , omitIfZero twoCol
         ( "    F Discount from deferral schemes"
         , foldOf (traverse . essDeferral) l )
-    , threeColLeft
+    , omitIfZero threeColLeft
         ( "    C TFN amounts withheld from discounts"
         , foldOf (traverse . essTFNAmounts) l )
-    , twoCol
+    , omitIfZero twoCol
         ( "    A Foreign source discounts"
         , foldOf (traverse . essForeignSourceDiscounts) l )
     ]
 
+summariseOtherIncome :: OtherIncome Rational -> P.Doc
+summariseOtherIncome oi | view taxableIncome oi == Money (0 :: Rational)
+                        = P.empty
+summariseOtherIncome oi =
+  P.vcat
+    [ "  24  Other income"
+    , case view otherIncomeCategory1 oi of
+        [] -> P.empty
+        [(s, x)] -> twoCol ("    Y - Category 1 - " <> P.text s, x)
+        l -> twoCol ("    Y - Category 1 - <multiple>", foldOf (traverse . _2) l)
+    , case view otherIncomeCategory2 oi of
+        [] -> P.empty
+        [(s, x)] -> twoCol ("    X - Category 2 (ATO interest) - " <> P.text s, x)
+        l -> twoCol ("    X - Category 2 (ATO interest) - <multiple>", foldOf (traverse . _2) l)
+    , omitIfZero twoCol ("    R - Category 3 (FHSS)", view otherIncomeCategory3 oi)
+    , case view otherIncomeCategory4 oi of
+        [] -> P.empty
+        [(s, x)] -> twoCol ("    V - Category 4 - " <> P.text s, x)
+        l -> twoCol ("    V - Category 4 - <multiple>", foldOf (traverse . _2) l)
+    , omitIfZero threeColLeft
+        ("    E Tax withheld - LSPIA", view taxWithheldLumpSumPaymentsInArrears oi)
+    , omitIfZero threeColLeft
+        ("    Z Taxable professional income", view taxableProfessionalIncome oi)
+    , omitIfZero threeColLeft
+        ("    S Tax withheld - FHSS released amount", view taxWithheldAssessableFHSSReleasedAmount oi)
+    ]
+
+summariseBPI :: BusinessAndProfessionalItemsSchedule Rational -> P.Doc
+summariseBPI bpi =
+  views personalServicesIncome summarisePSI bpi
+
+summarisePSI :: PersonalServicesIncome Rational -> P.Doc
+summarisePSI psi | netPSI psi == mempty
+                 = P.empty
+summarisePSI psi =
+  "  P1  Personal services income"
+  P.$+$ vcatWith (omitIfZero twoCol)
+    [ ("    M PSI - voluntary agreement", view psiVoluntaryAgreement psi)
+    , ("    N PSI - where Australian business number not quoted", view psiWhereABNNotQuoted psi)
+    , ("    O PSI - labour hire or other specified payments", view psiLabourHireOrOtherSpecifiedPayments psi)
+    , ("    J PSI - other", view psiOther psi)
+    , ("    K Deductions for payments to associates for principal work", view psiDeductionsForPaymentsToAssociatesForPrincipalWork psi)
+    , ("    L Total amount of other deductions against PSI", view psiTotalAmountOfOtherDeductions psi)
+    ]
+  P.$+$ twoCol ("    Net PSI", netPSI psi)
+
+summariseCGTSchedule :: CGTAssessment Rational -> P.Doc
+summariseCGTSchedule o = P.vcat
+  [ "1  Current year capital gains and capital losses"
+  , let (g,l) = view (cgtGainsAndLossesByCategory . at CGTScheduleCategorySharesAUListed . traverse) o
+    in print3 "A" "K" ("  Australian listed shares", g, l)
+  , let (g,l) = view (cgtGainsAndLossesByCategory . at CGTScheduleCategorySharesOther . traverse) o
+    in print3 "B" "L" ("  Other shares", g, l)
+  , let (g,l) = view (cgtGainsAndLossesByCategory . at CGTScheduleCategoryUnitsAUListed . traverse) o
+    in print3 "C" "M" ("  Australian listed units", g, l)
+  , let (g,l) = view (cgtGainsAndLossesByCategory . at CGTScheduleCategoryUnitsOther . traverse) o
+    in print3 "D" "N" ("  Other units", g, l)
+  , let (g,l) = view (cgtGainsAndLossesByCategory . at CGTScheduleCategoryRealEstateAU . traverse) o
+    in print3 "E" "O" ("  Australian real estate", g, l)
+  , let (g,l) = view (cgtGainsAndLossesByCategory . at CGTScheduleCategoryRealEstateOther . traverse) o
+    in print3 "F" "P" ("  Other real estate", g, l)
+  , let (g,_) = view (cgtGainsAndLossesByCategory . at CGTScheduleCategoryTrust . traverse) o
+    in print2_ "G" ("  Capital gains from trusts", g)
+  , let (g,l) = view (cgtGainsAndLossesByCategory . at CGTScheduleCategoryCollectable . traverse) o
+    in print3 "H" "Q" ("  Collectables", g, l)
+  , let (g,l) = view (cgtGainsAndLossesByCategory . at CGTScheduleCategoryOther . traverse) o
+    in print3 "I" "R" ("  Other CGT assets and CGT events", g, l)
+  , let (g,_) = view (cgtGainsAndLossesByCategory . at CGTScheduleCategoryOther . traverse) o
+    in print2_ "S" ("  Previously deferred CGT relief", g)
+  , print2_ "J" ("  Total current year capital gains",  view cgtTotalCurrentYearGains o)
+
+  , P.empty
+  , "2  Capital losses"
+  , print2 "A" ("  Total current year capital losses",  view cgtTotalCurrentYearLosses o)
+  , print2 "B" ("  Total current year capital losses applied",   view cgtTotalCurrentYearLossesApplied o)
+  , print2 "C" ("  Total prior year net capital losses applied", view cgtTotalPriorYearLossesApplied o)
+  , let total = view cgtTotalCurrentYearLossesApplied o <> view cgtTotalPriorYearLossesApplied o
+    in print2 "E" ("  Total capital losses applied", total)
+
+  , P.empty
+  , "3  Unapplied net capital losses carried forward"
+  , print2 "A"
+      ("  Net capital losses from collectables carried forward"
+      , view (capitalLossCarryForward . capitalLossCarryForwardCollectables) o)
+  , print2 "B"
+      ("  Other capital losses carried forward"
+      , view (capitalLossCarryForward . capitalLossCarryForwardOther) o)
+
+  , P.empty
+  , "4  CGT discount"
+  , print2 "A" ("  Total CGT discount applied", view cgtTotalCGTDiscountApplied o)
+
+  , P.empty
+  , "6  Net capital gain"
+  , print2 "A" ("  Net capital gain", view cgtNetGain o)
+  ]
+  where
+    prepLabel s = "  " <> s <> " $"
+    print3 labelL labelR =
+      threeCol' id
+        (prepend (prepLabel labelL) moneyFormatter)
+        (prepend (prepLabel labelR) moneyFormatter)
+    print2_ labelL (k, v) =
+      threeCol' id
+        (prepend (prepLabel labelL) moneyFormatter)
+        (blank $ prepend (prepLabel labelL) moneyFormatter)
+        (k, v, mempty)
+    print2 label = twoCol' id (prepend (prepLabel label) moneyFormatter)
+
 summariseCGT :: TaxReturnInfo y Rational -> P.Doc
 summariseCGT info
   | o == nullCGTAssessment = P.empty
@@ -150,7 +296,7 @@
         , threeColLeft  ("    V Net capital losses carried forward", view cgtNetLossesCarriedForward o)
         ]
   where
-    o = assessCGTEvents (view capitalLossCarryForward info) (view cgtEvents info)
+    o = assessCGTEvents Individual (view capitalLossCarryForward info) (view cgtEvents info)
 
 deductionsTable :: [(ALens' (Deductions Rational) (Money Rational), String)]
 deductionsTable =
@@ -207,4 +353,4 @@
     ]
   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)
+  P.$+$ "Net capital loss to carry forward" P.$$ P.nest colWidthLabel (views (taxCGTAssessment . cgtNetLossesCarriedForward) formatMoney assessment)
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:             2025.1
+version:             2026.1
 synopsis:            Tax types and computations for Australia
 description:
   This library provides types and tax computations for tax
@@ -11,10 +11,10 @@
 license-file:        LICENSE
 author:              Fraser Tweedale
 maintainer:          frase@frase.id.au
-copyright:           Copyright (C) 2018-2023 Fraser Tweedale
+copyright:           Copyright (C) 2018-2026 Fraser Tweedale
 category:            Finance
 build-type:          Simple
-tested-with:         GHC ==9.6.7 || ==9.8.4 || ==9.10.2 || ==9.12.2
+tested-with:         GHC ==9.6.7 || ==9.8.4 || ==9.10.3 || ==9.12.4 || ==9.14.1
 extra-source-files:
   .hlint.yaml
 extra-doc-files:
@@ -56,6 +56,7 @@
   exposed-modules:
     Data.Tax.ATO
     Data.Tax.ATO.ABN
+    Data.Tax.ATO.BPI
     Data.Tax.ATO.TFN
     Data.Tax.ATO.CGT
     Data.Tax.ATO.Common
@@ -74,8 +75,11 @@
     Data.Tax.ATO.FY.FY2023
     Data.Tax.ATO.FY.FY2024
     Data.Tax.ATO.FY.FY2025
+    Data.Tax.ATO.FY.FY2026
+    Data.Tax.ATO.FY.FY2027
   build-depends:
+    , containers >= 0.6 && < 0.9
     , lens >= 4.18 && < 6
-    , time >= 1.11 && < 1.15
+    , time >= 1.11 && < 1.16
     , tax >= 0.2 && < 0.3
     , pretty >= 1.1.3.6 && < 2
