ADD local budget interval

This commit is contained in:
Nathan Dwarshuis 2023-05-29 13:09:17 -04:00
parent 6117784d0e
commit 5dfbc3ef41
4 changed files with 117 additions and 71 deletions

View File

@ -19,8 +19,15 @@ import Database.Esqueleto.Experimental ((==.), (^.))
import qualified Database.Esqueleto.Experimental as E import qualified Database.Esqueleto.Experimental as E
import Database.Esqueleto.Internal.Internal (SqlSelect) import Database.Esqueleto.Internal.Internal (SqlSelect)
import Database.Persist.Monad import Database.Persist.Monad
-- import Database.Persist.Sql hiding (delete, runMigration, (==.), (||.)) import Database.Persist.Sqlite hiding
import Database.Persist.Sqlite hiding (delete, deleteWhere, insert, insertKey, runMigration, (==.), (||.)) ( delete
, deleteWhere
, insert
, insertKey
, runMigration
, (==.)
, (||.)
)
import GHC.Err import GHC.Err
import Internal.Types import Internal.Types
import Internal.Utils import Internal.Utils

View File

@ -95,6 +95,22 @@ expandMDYPat lower upper (Repeat RepeatPat {rpStart = s, rpBy = b, rpRepeats = r
dayToWeekday :: Day -> Int dayToWeekday :: Day -> Int
dayToWeekday (ModifiedJulianDay d) = mod (fromIntegral d + 2) 7 dayToWeekday (ModifiedJulianDay d) = mod (fromIntegral d + 2) 7
askDays
:: (MonadFinance m, MonadInsertError m)
=> DatePat
-> Maybe Interval
-> m [Day]
askDays dp i = do
globalBounds <- askDBState kmBudgetInterval
case i of
Just i' -> do
localBounds <- liftExcept $ resolveBounds i'
let bounds = intersectBounds globalBounds localBounds
maybe (return []) expand bounds
Nothing -> expand globalBounds
where
expand bs = liftExcept $ expandDatePat bs dp
withDates withDates
:: (MonadSqlQuery m, MonadFinance m, MonadInsertError m) :: (MonadSqlQuery m, MonadFinance m, MonadInsertError m)
=> DatePat => DatePat
@ -105,15 +121,13 @@ withDates dp f = do
days <- liftExcept $ expandDatePat bounds dp days <- liftExcept $ expandDatePat bounds dp
combineErrors $ fmap f days combineErrors $ fmap f days
foldDates foldDays
:: (MonadSqlQuery m, MonadFinance m, MonadInsertError m) :: MonadInsertError m
=> DatePat => (Day -> Day -> m a)
-> Day -> Day
-> (Day -> Day -> m a) -> [Day]
-> m [a] -> m [a]
foldDates dp start f = do foldDays f start days =
bounds <- askDBState kmBudgetInterval
days <- liftExcept $ expandDatePat bounds dp
combineErrors $ combineErrors $
snd $ snd $
L.mapAccumL (\prevDay day -> (day, f prevDay day)) start days L.mapAccumL (\prevDay day -> (day, f prevDay day)) start days
@ -145,11 +159,12 @@ insertBudget
, bgtPretax , bgtPretax
, bgtTax , bgtTax
, bgtPosttax , bgtPosttax
, bgtInterval
} = } =
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 bgtInterval) bgtIncomes
let res2 = expandTransfers key bgtLabel bgtTransfers let res2 = expandTransfers key bgtLabel bgtInterval bgtTransfers
txs <- combineError (concat <$> res1) res2 (++) txs <- combineError (concat <$> res1) res2 (++)
m <- askDBState kmCurrency m <- askDBState kmCurrency
shadow <- liftExcept $ addShadowTransfers m bgtShadowTransfers txs shadow <- liftExcept $ addShadowTransfers m bgtShadowTransfers txs
@ -160,7 +175,7 @@ insertBudget
post_ = sortAllos bgtPosttax post_ = sortAllos bgtPosttax
sortAllos = liftExcept . combineErrors . fmap sortAllo sortAllos = liftExcept . combineErrors . fmap sortAllo
type BoundAllocation = Allocation (Day, Day) type BoundAllocation = Allocation Bounds
type IntAllocations = type IntAllocations =
( [BoundAllocation PretaxValue] ( [BoundAllocation PretaxValue]
@ -180,7 +195,7 @@ sortAllo a@Allocation {alloAmts = as} = do
res <- case xs of res <- case xs of
[] -> resolveBounds start [] -> resolveBounds start
(y : _) -> resolveBounds_ (intStart $ amtWhen y) start (y : _) -> resolveBounds_ (intStart $ amtWhen y) start
foldBounds (x {amtWhen = expandBounds res} : acc) xs foldBounds (x {amtWhen = 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
@ -209,35 +224,35 @@ fromShadow cm tx t@ShadowTransfer {stFrom, stTo, stDesc, stRatio, stCurrency, st
Just $ Just $
-- TODO does this actually share the same metadata as the "parent" tx? -- TODO does this actually share the same metadata as the "parent" tx?
FlatTransfer FlatTransfer
{ cbtMeta = cbtMeta tx { ftMeta = ftMeta tx
, cbtWhen = cbtWhen tx , ftWhen = ftWhen tx
, cbtCur = stCurrency , ftCur = stCurrency
, cbtFrom = stFrom , ftFrom = stFrom
, cbtTo = stTo , ftTo = stTo
, cbtValue = UnbalancedValue stType $ v * cvValue (cbtValue tx) , ftValue = UnbalancedValue stType $ v * cvValue (ftValue tx)
, cbtDesc = stDesc , ftDesc = stDesc
} }
shadowMatches :: TransferMatcher -> UnbalancedTransfer -> InsertExcept Bool shadowMatches :: TransferMatcher -> UnbalancedTransfer -> InsertExcept Bool
shadowMatches TransferMatcher {tmFrom, tmTo, tmDate, tmVal} tx = do shadowMatches TransferMatcher {tmFrom, tmTo, tmDate, tmVal} tx = do
valRes <- valMatches tmVal $ cvValue $ cbtValue tx valRes <- valMatches tmVal $ cvValue $ ftValue tx
return $ return $
memberMaybe (taAcnt $ cbtFrom tx) tmFrom memberMaybe (taAcnt $ ftFrom tx) tmFrom
&& memberMaybe (taAcnt $ cbtTo tx) tmTo && memberMaybe (taAcnt $ ftTo tx) tmTo
&& maybe True (`dateMatches` cbtWhen tx) tmDate && maybe True (`dateMatches` ftWhen tx) tmDate
&& valRes && valRes
where where
memberMaybe x AcntSet {asList, asInclude} = memberMaybe x AcntSet {asList, asInclude} =
(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 = snd . L.mapAccumR go M.empty . reverse . L.sortOn cbtWhen balanceTransfers = snd . L.mapAccumR go M.empty . reverse . L.sortOn ftWhen
where where
go bals f@FlatTransfer {cbtFrom, cbtTo, cbtValue = UnbalancedValue {cvValue, cvType}} = go bals f@FlatTransfer {ftFrom, ftTo, ftValue = UnbalancedValue {cvValue, cvType}} =
let balTo = M.findWithDefault 0 cbtTo bals let balTo = M.findWithDefault 0 ftTo bals
x = amtToMove balTo cvType cvValue x = amtToMove balTo cvType cvValue
bals' = mapAdd_ cbtTo x $ mapAdd_ cbtFrom (-x) bals bals' = mapAdd_ ftTo x $ mapAdd_ ftFrom (-x) bals
in (bals', f {cbtValue = x}) in (bals', f {ftValue = 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
-- target value -- target value
@ -255,13 +270,13 @@ data BudgetMeta = BudgetMeta
deriving (Show) deriving (Show)
data FlatTransfer v = FlatTransfer data FlatTransfer v = FlatTransfer
{ cbtFrom :: !TaggedAcnt { ftFrom :: !TaggedAcnt
, cbtTo :: !TaggedAcnt , ftTo :: !TaggedAcnt
, cbtValue :: !v , ftValue :: !v
, cbtWhen :: !Day , ftWhen :: !Day
, cbtDesc :: !T.Text , ftDesc :: !T.Text
, cbtMeta :: !BudgetMeta , ftMeta :: !BudgetMeta
, cbtCur :: !BudgetCurrency , ftCur :: !BudgetCurrency
} }
deriving (Show) deriving (Show)
@ -276,16 +291,18 @@ type UnbalancedTransfer = FlatTransfer UnbalancedValue
type BalancedTransfer = FlatTransfer Rational type BalancedTransfer = FlatTransfer Rational
insertIncome insertIncome
:: (MonadInsertError m, MonadSqlQuery m, MonadFinance m) :: (MonadInsertError m, MonadFinance m)
=> CommitRId => CommitRId
-> T.Text -> T.Text
-> IntAllocations -> IntAllocations
-> Maybe Interval
-> Income -> Income
-> m [UnbalancedTransfer] -> m [UnbalancedTransfer]
insertIncome insertIncome
key key
name name
(intPre, intTax, intPost) (intPre, intTax, intPost)
localInterval
Income Income
{ incWhen { incWhen
, incCurrency , incCurrency
@ -304,7 +321,8 @@ insertIncome
-- TODO this will scan the interval allocations fully each time -- TODO this will scan the interval allocations fully each time
-- iteration which is a total waste, but the fix requires turning this -- iteration which is a total waste, but the fix requires turning this
-- loop into a fold which I don't feel like doing now :( -- loop into a fold which I don't feel like doing now :(
res <- foldDates incWhen start (allocate precision gross) days <- askDays incWhen localInterval
res <- foldDays (allocate precision gross) start days
return $ concat res return $ concat res
where where
start = fromGregorian' $ pStart incPayPeriod start = fromGregorian' $ pStart incPayPeriod
@ -330,13 +348,13 @@ insertIncome
balance = aftertaxGross - sumAllos post balance = aftertaxGross - sumAllos post
bal = bal =
FlatTransfer FlatTransfer
{ cbtMeta = meta { ftMeta = meta
, cbtWhen = day , ftWhen = day
, cbtFrom = incFrom , ftFrom = incFrom
, cbtCur = NoX incCurrency , ftCur = NoX incCurrency
, cbtTo = incToBal , ftTo = incToBal
, cbtValue = UnbalancedValue BTFixed balance , ftValue = UnbalancedValue BTFixed balance
, cbtDesc = "balance after deductions" , ftDesc = "balance after deductions"
} }
in if balance < 0 in if balance < 0
then throwError $ InsertException [IncomeError day name balance] then throwError $ InsertException [IncomeError day name balance]
@ -404,13 +422,13 @@ allo2Trans
-> UnbalancedTransfer -> UnbalancedTransfer
allo2Trans meta day from FlatAllocation {faValue, faTo, faDesc, faCur} = allo2Trans meta day from FlatAllocation {faValue, faTo, faDesc, faCur} =
FlatTransfer FlatTransfer
{ cbtMeta = meta { ftMeta = meta
, cbtWhen = day , ftWhen = day
, cbtFrom = from , ftFrom = from
, cbtCur = faCur , ftCur = faCur
, cbtTo = faTo , ftTo = faTo
, cbtValue = UnbalancedValue BTFixed faValue , ftValue = UnbalancedValue BTFixed faValue
, cbtDesc = faDesc , ftDesc = faDesc
} }
allocateTax allocateTax
@ -501,12 +519,19 @@ expandTransfers
:: (MonadInsertError m, MonadSqlQuery m, MonadFinance m) :: (MonadInsertError m, MonadSqlQuery m, MonadFinance m)
=> CommitRId => CommitRId
-> T.Text -> T.Text
-> Maybe Interval
-> [BudgetTransfer] -> [BudgetTransfer]
-> m [UnbalancedTransfer] -> m [UnbalancedTransfer]
expandTransfers key name ts = expandTransfers key name localInterval ts = do
fmap (L.sortOn cbtWhen . concat) $ txs <-
combineErrors $ fmap (L.sortOn ftWhen . concat) $
fmap (expandTransfer key name) ts combineErrors $
fmap (expandTransfer key name) ts
case localInterval of
Nothing -> return txs
Just i -> do
bounds <- liftExcept $ resolveBounds i
return $ filter (inBounds bounds . ftWhen) txs
initialCurrency :: BudgetCurrency -> CurID initialCurrency :: BudgetCurrency -> CurID
initialCurrency (NoX c) = c initialCurrency (NoX c) = c
@ -533,28 +558,28 @@ expandTransfer key name Transfer {transAmounts, transTo, transCurrency, transFro
let meta = BudgetMeta {bmCommit = key, bmName = name} let meta = BudgetMeta {bmCommit = key, bmName = name}
return return
FlatTransfer FlatTransfer
{ cbtMeta = meta { ftMeta = meta
, cbtWhen = day , ftWhen = day
, cbtCur = transCurrency , ftCur = transCurrency
, cbtFrom = transFrom , ftFrom = transFrom
, cbtTo = transTo , ftTo = transTo
, cbtValue = UnbalancedValue y $ roundPrecision precision v , ftValue = UnbalancedValue y $ roundPrecision precision v
, cbtDesc = desc , ftDesc = desc
} }
insertBudgetTx :: (MonadInsertError m, MonadSqlQuery m, MonadFinance m) => BalancedTransfer -> m () insertBudgetTx :: (MonadInsertError m, MonadSqlQuery m, MonadFinance m) => BalancedTransfer -> m ()
insertBudgetTx FlatTransfer {cbtFrom, cbtTo, cbtMeta, cbtCur, cbtValue, cbtDesc, cbtWhen} = do insertBudgetTx FlatTransfer {ftFrom, ftTo, ftMeta, ftCur, ftValue, ftDesc, ftWhen} = do
((sFrom, sTo), exchange) <- splitPair cbtFrom cbtTo cbtCur cbtValue ((sFrom, sTo), exchange) <- splitPair ftFrom ftTo ftCur ftValue
insertPair sFrom sTo insertPair sFrom sTo
forM_ exchange $ uncurry insertPair forM_ exchange $ uncurry insertPair
where where
insertPair from to = do insertPair from to = do
k <- insert $ TransactionR (bmCommit cbtMeta) cbtWhen cbtDesc k <- insert $ TransactionR (bmCommit ftMeta) ftWhen ftDesc
insertBudgetLabel k from insertBudgetLabel k from
insertBudgetLabel k to insertBudgetLabel k to
insertBudgetLabel k split = do insertBudgetLabel k split = do
sk <- insertSplit k split sk <- insertSplit k split
insert_ $ BudgetLabelR sk $ bmName cbtMeta insert_ $ BudgetLabelR sk $ bmName ftMeta
type SplitPair = (KeySplit, KeySplit) type SplitPair = (KeySplit, KeySplit)
@ -646,7 +671,7 @@ insertHistTransfer
readHistStmt :: (MonadUnliftIO m, MonadFinance m) => Statement -> m (Maybe (CommitR, [KeyTx])) readHistStmt :: (MonadUnliftIO m, MonadFinance m) => Statement -> m (Maybe (CommitR, [KeyTx]))
readHistStmt i = whenHash_ CTImport i $ do readHistStmt i = whenHash_ CTImport i $ do
bs <- readImport i bs <- readImport i
bounds <- expandBounds <$> askDBState kmStatementInterval bounds <- askDBState kmStatementInterval
liftIOExceptT $ mapErrors resolveTx $ filter (inBounds bounds . txDate) bs liftIOExceptT $ mapErrors resolveTx $ filter (inBounds bounds . txDate) bs
insertHistStmt :: (MonadSqlQuery m) => CommitR -> [KeyTx] -> m () insertHistStmt :: (MonadSqlQuery m) => CommitR -> [KeyTx] -> m ()

View File

@ -200,6 +200,7 @@ data Budget = Budget
, bgtPosttax :: [MultiAllocation PosttaxValue] , bgtPosttax :: [MultiAllocation PosttaxValue]
, bgtTransfers :: [BudgetTransfer] , bgtTransfers :: [BudgetTransfer]
, bgtShadowTransfers :: [ShadowTransfer] , bgtShadowTransfers :: [ShadowTransfer]
, bgtInterval :: !(Maybe Interval)
} }
deriving instance Hashable PretaxValue deriving instance Hashable PretaxValue

View File

@ -8,6 +8,7 @@ module Internal.Utils
, fromGregorian' , fromGregorian'
, resolveBounds , resolveBounds
, resolveBounds_ , resolveBounds_
, intersectBounds
, liftInner , liftInner
, liftExceptT , liftExceptT
, liftExcept , liftExcept
@ -161,14 +162,26 @@ compareDate (In md offset) x = do
fromGregorian' :: Gregorian -> Day fromGregorian' :: Gregorian -> Day
fromGregorian' = uncurry3 fromGregorian . gregTup fromGregorian' = uncurry3 fromGregorian . gregTup
-- TODO misleading name inBounds :: Bounds -> Day -> Bool
inBounds :: (Day, Day) -> Day -> Bool inBounds bs = withinDays (expandBounds bs)
inBounds (d0, d1) x = d0 <= x && x < d1
withinDays :: (Day, Day) -> Day -> Bool
withinDays (d0, d1) x = d0 <= x && x < d1
resolveBounds :: Interval -> InsertExcept Bounds resolveBounds :: Interval -> InsertExcept Bounds
resolveBounds i@Interval {intStart = s} = resolveBounds i@Interval {intStart = s} =
resolveBounds_ (s {gYear = gYear s + 50}) i resolveBounds_ (s {gYear = gYear s + 50}) i
-- TODO not DRY
intersectBounds :: Bounds -> Bounds -> Maybe Bounds
intersectBounds a b =
if b' > a' then Nothing else Just (a', fromIntegral $ diffDays b' a' - 1)
where
(a0, a1) = expandBounds a
(b0, b1) = expandBounds b
a' = max a0 a1
b' = min b0 b1
resolveBounds_ :: Gregorian -> Interval -> InsertExcept Bounds resolveBounds_ :: Gregorian -> Interval -> InsertExcept Bounds
resolveBounds_ def Interval {intStart = s, intEnd = e} = resolveBounds_ def Interval {intStart = s, intEnd = e} =
case fromGregorian' <$> e of case fromGregorian' <$> e of