FIX tax miscalculations

This commit is contained in:
Nathan Dwarshuis 2023-05-16 23:12:29 -04:00
parent 5e2e8d8acf
commit b2fdc8d74c
2 changed files with 61 additions and 47 deletions

View File

@ -135,7 +135,7 @@ foldDates dp start f = do
insertBudget insertBudget
:: (MonadInsertError m, MonadSqlQuery m, MonadFinance m) :: (MonadInsertError m, MonadSqlQuery m, MonadFinance m)
=> Budget => Budget
-> m [UnbalancedTransfer] -> m ()
insertBudget insertBudget
b@Budget b@Budget
{ bgtLabel { bgtLabel
@ -146,7 +146,7 @@ insertBudget
, bgtTax , bgtTax
, bgtPosttax , bgtPosttax
} = } =
whenHash CTBudget b [] $ \key -> do whenHash CTBudget b () $ \key -> do
intAllos <- combineError3 pre_ tax_ post_ (,,) intAllos <- combineError3 pre_ tax_ post_ (,,)
let res1 = mapErrors (insertIncome key bgtLabel intAllos) bgtIncomes let res1 = mapErrors (insertIncome key bgtLabel intAllos) bgtIncomes
let res2 = expandTransfers key bgtLabel bgtTransfers let res2 = expandTransfers key bgtLabel bgtTransfers
@ -154,7 +154,6 @@ insertBudget
m <- askDBState kmCurrency m <- askDBState kmCurrency
shadow <- liftExcept $ addShadowTransfers m bgtShadowTransfers txs shadow <- liftExcept $ addShadowTransfers m bgtShadowTransfers txs
void $ mapErrors insertBudgetTx $ balanceTransfers $ txs ++ shadow void $ mapErrors insertBudgetTx $ balanceTransfers $ txs ++ shadow
return $ shadow ++ txs
where where
pre_ = sortAllos bgtPretax pre_ = sortAllos bgtPretax
tax_ = sortAllos bgtTax tax_ = sortAllos bgtTax
@ -172,16 +171,16 @@ type IntAllocations =
-- TODO this should actually error if there is no ultimate end date? -- TODO this should actually error if there is no ultimate end date?
sortAllo :: MultiAllocation v -> InsertExcept (BoundAllocation v) sortAllo :: MultiAllocation v -> InsertExcept (BoundAllocation v)
sortAllo a@Allocation {alloAmts = as} = do sortAllo a@Allocation {alloAmts = as} = do
bs <- foldBounds (return []) $ L.sortOn amtWhen as bs <- foldBounds [] $ L.sortOn amtWhen as
return $ a {alloAmts = reverse bs} return $ a {alloAmts = reverse bs}
where where
foldBounds acc [] = acc foldBounds acc [] = return acc
foldBounds acc (x : xs) = foldBounds acc (x : xs) = do
let res = case xs of let start = amtWhen x
[] -> resolveBounds $ amtWhen x res <- case xs of
(y : _) -> resolveBounds_ (intStart $ amtWhen y) $ amtWhen x [] -> resolveBounds start
concatRes bs acc' = x {amtWhen = expandBounds bs} : acc' (y : _) -> resolveBounds_ (intStart $ amtWhen y) start
in foldBounds (combineError res acc concatRes) xs foldBounds (x {amtWhen = expandBounds res} : acc) xs
-- TODO this is going to be O(n*m), which might be a problem? -- TODO this is going to be O(n*m), which might be a problem?
addShadowTransfers addShadowTransfers
@ -232,12 +231,12 @@ shadowMatches TransferMatcher {tmFrom, tmTo, tmDate, tmVal} tx = do
(if asInclude then id else not) $ x `elem` asList (if asInclude then id else not) $ x `elem` asList
balanceTransfers :: [UnbalancedTransfer] -> [BalancedTransfer] balanceTransfers :: [UnbalancedTransfer] -> [BalancedTransfer]
balanceTransfers ts = balanceTransfers = snd . L.mapAccumR go M.empty . reverse . L.sortOn cbtWhen
snd $ L.mapAccumR go M.empty $ reverse $ L.sortOn cbtWhen ts
where where
go bals f@FlatTransfer {cbtFrom, cbtTo, cbtValue = UnbalancedValue {cvValue, cvType}} = go bals f@FlatTransfer {cbtFrom, cbtTo, cbtValue = UnbalancedValue {cvValue, cvType}} =
let (bals', v) = mapAdd cbtTo x $ mapAdd_ cbtFrom (-x) bals let balTo = M.findWithDefault 0 cbtTo bals
x = amtToMove v cvType cvValue x = amtToMove balTo cvType cvValue
bals' = mapAdd_ cbtTo x $ mapAdd_ cbtFrom (-x) bals
in (bals', f {cbtValue = x}) in (bals', f {cbtValue = x})
-- TODO might need to query signs to make this intuitive; as it is this will -- TODO might need to query signs to make this intuitive; as it is this will
-- probably work, but for credit accounts I might need to supply a negative -- probably work, but for credit accounts I might need to supply a negative
@ -247,12 +246,7 @@ balanceTransfers ts =
amtToMove bal BTTarget x = x - bal amtToMove bal BTTarget x = x - bal
mapAdd_ :: (Ord k, Num v) => k -> v -> M.Map k v -> M.Map k v mapAdd_ :: (Ord k, Num v) => k -> v -> M.Map k v -> M.Map k v
mapAdd_ k v m = fst $ mapAdd k v m mapAdd_ k v = M.alter (maybe (Just v) (Just . (+ v))) k
mapAdd :: (Ord k, Num v) => k -> v -> M.Map k v -> (M.Map k v, v)
mapAdd k v m = (new, M.findWithDefault (error "this should not happen") k new)
where
new = M.alter (maybe (Just v) (Just . (+ v))) k m
data BudgetMeta = BudgetMeta data BudgetMeta = BudgetMeta
{ bmCommit :: !CommitRId { bmCommit :: !CommitRId
@ -356,25 +350,14 @@ periodScaler
-> Day -> Day
-> Day -> Day
-> InsertExcept PeriodScaler -> InsertExcept PeriodScaler
periodScaler pt prev cur periodScaler pt prev cur = do
| interval > 0 = return scale n <- workingDays wds prev cur
-- TODO fix error here return $ scale (fromIntegral n)
| otherwise = throwError $ InsertException undefined
where where
interval = diffDays cur prev wds = case pt of
startDay = dayOfWeek prev Hourly HourlyPeriod {hpWorkingDays} -> hpWorkingDays
days = L.sort $ Daily ds -> ds
fmap (diff startDay . fromWeekday) $ scale n precision x = case pt of
L.nub $ case pt of
Hourly HourlyPeriod {hpWorkingDays} -> hpWorkingDays
Daily ds -> ds
diff a b = fromIntegral $ mod (fromEnum a - fromEnum b) 7
n =
let (nFull, nPart) = divMod interval 7
daysFull = fromIntegral (length days) * nFull
daysTail = fromIntegral $ length $ takeWhile (< nPart) days
in fromIntegral $ daysFull + daysTail
scale precision x = case pt of
Hourly HourlyPeriod {hpAnnualHours, hpDailyHours} -> Hourly HourlyPeriod {hpAnnualHours, hpDailyHours} ->
fromRational (rnd $ x / fromIntegral hpAnnualHours) fromRational (rnd $ x / fromIntegral hpAnnualHours)
* fromIntegral hpDailyHours * fromIntegral hpDailyHours
@ -383,6 +366,20 @@ periodScaler pt prev cur
where where
rnd = roundPrecision precision rnd = roundPrecision precision
workingDays :: [Weekday] -> Day -> Day -> InsertExcept Natural
workingDays wds start end
| interval > 0 =
let (nFull, nPart) = divMod interval 7
daysFull = fromIntegral (length wds') * nFull
daysTail = fromIntegral $ length $ takeWhile (< nPart) wds'
in return $ fromIntegral $ daysFull + daysTail
| otherwise = throwError $ InsertException undefined
where
interval = diffDays end start
startDay = dayOfWeek start
wds' = L.sort $ (\x -> diff (fromWeekday x) startDay) <$> L.nub wds
diff a b = fromIntegral $ mod (fromEnum a - fromEnum b) 7
allocatePre allocatePre
:: Natural :: Natural
-> Rational -> Rational
@ -428,7 +425,10 @@ allocateTax precision gross preDeds f = fmap (fmap go)
go TaxValue {tvCategories, tvMethod} = go TaxValue {tvCategories, tvMethod} =
let agi = gross - sum (mapMaybe (`M.lookup` preDeds) tvCategories) let agi = gross - sum (mapMaybe (`M.lookup` preDeds) tvCategories)
in case tvMethod of in case tvMethod of
TMPercent p -> roundPrecision 3 p / 100 * agi TMPercent p ->
roundPrecision precision $
fromRational $
roundPrecision 3 p / 100 * agi
TMBracket TaxProgression {tpDeductible, tpBrackets} -> TMBracket TaxProgression {tpDeductible, tpBrackets} ->
let taxDed = roundPrecision precision $ f precision tpDeductible let taxDed = roundPrecision precision $ f precision tpDeductible
in foldBracket f precision (agi - taxDed) tpBrackets in foldBracket f precision (agi - taxDed) tpBrackets
@ -446,13 +446,24 @@ allocatePost precision aftertax = fmap (fmap go)
then aftertax * roundPrecision 3 v / 100 then aftertax * roundPrecision 3 v / 100
else roundPrecision precision v else roundPrecision precision v
-- | Compute effective tax percentage of a bracket
-- The algorithm can be thought of in three phases:
-- 1. Find the highest tax bracket by looping backward until the AGI is less
-- than the bracket limit
-- 2. Computing the tax in the top bracket by subtracting the AGI from the
-- bracket limit and multiplying by the tax percentage.
-- 3. Adding all lower brackets, which are just the limit of the bracket less
-- the amount of the lower bracket times the percentage.
--
-- In reality, this can all be done with one loop, but it isn't clear these
-- three steps are implemented from this alone.
foldBracket :: PeriodScaler -> Natural -> Rational -> [TaxBracket] -> Rational foldBracket :: PeriodScaler -> Natural -> Rational -> [TaxBracket] -> Rational
foldBracket f precision agi bs = fst $ foldr go (0, agi) $ L.sortOn tbLowerLimit bs foldBracket f precision agi bs = fst $ foldr go (0, agi) $ L.sortOn tbLowerLimit bs
where where
go TaxBracket {tbLowerLimit, tbPercent} (acc, remain) = go TaxBracket {tbLowerLimit, tbPercent} a@(acc, remain) =
let l = roundPrecision precision $ f precision tbLowerLimit let l = roundPrecision precision $ f precision tbLowerLimit
p = roundPrecision 3 tbPercent / 100 p = roundPrecision 3 tbPercent / 100
in if remain < l then (acc + p * (remain - l), l) else (acc, remain) in if remain >= l then (acc + p * (remain - l), l) else a
data FlatAllocation v = FlatAllocation data FlatAllocation v = FlatAllocation
{ faValue :: !v { faValue :: !v
@ -460,7 +471,7 @@ data FlatAllocation v = FlatAllocation
, faTo :: !TaggedAcnt , faTo :: !TaggedAcnt
, faCur :: !BudgetCurrency , faCur :: !BudgetCurrency
} }
deriving (Functor) deriving (Functor, Show)
flattenAllo :: SingleAllocation v -> [FlatAllocation v] flattenAllo :: SingleAllocation v -> [FlatAllocation v]
flattenAllo Allocation {alloAmts, alloCur, alloTo} = fmap go alloAmts flattenAllo Allocation {alloAmts, alloCur, alloTo} = fmap go alloAmts
@ -476,9 +487,7 @@ flattenAllo Allocation {alloAmts, alloCur, alloTo} = fmap go alloAmts
-- ASSUME allocations are sorted -- ASSUME allocations are sorted
selectAllos :: Day -> BoundAllocation v -> [FlatAllocation v] selectAllos :: Day -> BoundAllocation v -> [FlatAllocation v]
selectAllos day Allocation {alloAmts, alloCur, alloTo} = selectAllos day Allocation {alloAmts, alloCur, alloTo} =
fmap go $ go <$> filter ((`inBounds` day) . amtWhen) alloAmts
takeWhile ((`inBounds` day) . amtWhen) $
dropWhile ((day <) . fst . amtWhen) alloAmts
where where
go Amount {amtValue, amtDesc} = go Amount {amtValue, amtDesc} =
FlatAllocation FlatAllocation

View File

@ -293,7 +293,12 @@ fromPersistText what (PersistText t) = case readMaybe $ T.unpack t of
fromPersistText what x = fromPersistText what x =
Left $ T.unwords ["error when deserializing", what, "; got", T.pack (show x)] Left $ T.unwords ["error when deserializing", what, "; got", T.pack (show x)]
deriving instance Ord Interval -- this is necessary since dhall will reverse the order when importing
instance Ord Interval where
compare
Interval {intStart = s0, intEnd = e0}
Interval {intStart = s1, intEnd = e1} =
compare (s0, e0) (s1, e1)
data Transfer a c w v = Transfer data Transfer a c w v = Transfer
{ transFrom :: a { transFrom :: a