FIX tax miscalculations
This commit is contained in:
parent
5e2e8d8acf
commit
b2fdc8d74c
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue