REF move commont stuff to common modules

This commit is contained in:
Nathan Dwarshuis 2023-07-01 18:58:15 -04:00
parent ebef4e0f6b
commit d5761c75ed
5 changed files with 409 additions and 480 deletions

View File

@ -163,25 +163,26 @@ runDumpAccountKeys c = do
runSync :: FilePath -> IO () runSync :: FilePath -> IO ()
runSync c = do runSync c = do
config <- readConfig c config <- readConfig c
let (hTs, hSs) = splitHistory $ statements config
pool <- runNoLoggingT $ mkPool $ sqlConfig config pool <- runNoLoggingT $ mkPool $ sqlConfig config
handle err $ do handle err $ do
-- _ <- askLoggerIO -- _ <- askLoggerIO
-- get the current DB state -- Get the current DB state.
(state, updates) <- runSqlQueryT pool $ do (state, updates) <- runSqlQueryT pool $ do
runMigration migrateAll runMigration migrateAll
liftIOExceptT $ getDBState config liftIOExceptT $ getDBState config
-- read desired statements from disk -- Read raw transactions according to state. If a transaction is already in
-- the database, don't read it but record the commit so we can update it.
(rus, is) <- (rus, is) <-
flip runReaderT state $ do flip runReaderT state $ do
let (hTs, hSs) = splitHistory $ statements config
hSs' <- mapErrorsIO (readHistStmt root) hSs hSs' <- mapErrorsIO (readHistStmt root) hSs
hTs' <- liftIOExceptT $ mapErrors readHistTransfer hTs hTs' <- liftIOExceptT $ mapErrors readHistTransfer hTs
bTs <- liftIOExceptT $ mapErrors readBudget $ budget config bTs <- liftIOExceptT $ mapErrors readBudget $ budget config
return $ second concat $ partitionEithers $ hSs' ++ hTs' ++ bTs return $ second concat $ partitionEithers $ hSs' ++ hTs' ++ bTs
-- update the DB -- Update the DB.
runSqlQueryT pool $ withTransaction $ flip runReaderT state $ do runSqlQueryT pool $ withTransaction $ flip runReaderT state $ do
res <- runExceptT $ do res <- runExceptT $ do
-- TODO taking out the hash is dumb -- TODO taking out the hash is dumb

View File

@ -3,7 +3,6 @@ module Internal.Budget (readBudget) where
import Control.Monad.Except import Control.Monad.Except
import Data.Foldable import Data.Foldable
import Internal.Database import Internal.Database
import Internal.History
import Internal.Types.Main import Internal.Types.Main
import Internal.Utils import Internal.Utils
import RIO hiding (to) import RIO hiding (to)
@ -13,17 +12,6 @@ import qualified RIO.NonEmpty as NE
import qualified RIO.Text as T import qualified RIO.Text as T
import RIO.Time import RIO.Time
-- each budget (designated at the top level by a 'name') is processed in the
-- following steps
-- 1. expand all transactions given the desired date range and date patterns for
-- each directive in the budget
-- 2. sort all transactions by date
-- 3. propagate all balances forward, and while doing so assign values to each
-- transaction (some of which depend on the 'current' balance of the
-- target account)
-- 4. assign shadow transactions
-- 5. insert all transactions
readBudget readBudget
:: (MonadInsertError m, MonadFinance m) :: (MonadInsertError m, MonadFinance m)
=> Budget => Budget
@ -47,7 +35,7 @@ readBudget
(intAllos, _) <- combineError intAlloRes acntRes (,) (intAllos, _) <- combineError intAlloRes acntRes (,)
let tc = BudgetCommit key bgtLabel let tc = BudgetCommit key bgtLabel
let res1 = mapErrors (readIncome tc intAllos budgetSpan) bgtIncomes let res1 = mapErrors (readIncome tc intAllos budgetSpan) bgtIncomes
let res2 = expandTransfers tc (Just budgetSpan) bgtTransfers let res2 = expandTransfers tc budgetSpan bgtTransfers
txs <- combineError (concat <$> res1) res2 (++) txs <- combineError (concat <$> res1) res2 (++)
shadow <- addShadowTransfers bgtShadowTransfers txs shadow <- addShadowTransfers bgtShadowTransfers txs
return $ txs ++ shadow return $ txs ++ shadow
@ -354,9 +342,6 @@ 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
--------------------------------------------------------------------------------
-- Standalone Transfer
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- shadow transfers -- shadow transfers
@ -403,12 +388,6 @@ shadowMatches TransferMatcher {tmFrom, tmTo, tmDate} Tx {txPrimary, txDate} = do
alloAcnt :: Allocation w v -> AcntID alloAcnt :: Allocation w v -> AcntID
alloAcnt = taAcnt . alloTo alloAcnt = taAcnt . alloTo
data UnbalancedValue = UnbalancedValue
{ cvType :: !TransferType
, cvValue :: !Rational
}
deriving (Show)
type IntAllocations = type IntAllocations =
( [DaySpanAllocation PretaxValue] ( [DaySpanAllocation PretaxValue]
, [DaySpanAllocation TaxValue] , [DaySpanAllocation TaxValue]

View File

@ -15,6 +15,8 @@ module Internal.Database
, insertEntry , insertEntry
, resolveEntry , resolveEntry
, readUpdates , readUpdates
, insertAll
, updateTx
) )
where where
@ -33,6 +35,7 @@ import Database.Persist.Sqlite hiding
, insertKey , insertKey
, insert_ , insert_
, runMigration , runMigration
, update
, (==.) , (==.)
, (||.) , (||.)
) )
@ -598,3 +601,33 @@ makeRoUE e = makeUE () e $ StaticValue (entryRValue e)
makeUnkUE :: EntryRId -> EntryR -> UpdateEntry EntryRId () makeUnkUE :: EntryRId -> EntryR -> UpdateEntry EntryRId ()
makeUnkUE k e = makeUE k e () makeUnkUE k e = makeUE k e ()
insertAll
:: (MonadInsertError m, MonadSqlQuery m, MonadFinance m)
=> [EntryBin]
-> m ()
insertAll ebs = do
(toUpdate, toInsert) <- balanceTxs ebs
mapM_ updateTx toUpdate
forM_ (groupWith itxCommit toInsert) $
\(c, ts) -> do
ck <- insert $ getCommit c
mapM_ (insertTx ck) ts
where
getCommit (HistoryCommit c) = c
getCommit (BudgetCommit c _) = c
insertTx :: MonadSqlQuery m => CommitRId -> InsertTx -> m ()
insertTx c InsertTx {itxDate, itxDescr, itxEntries, itxCommit} = do
let anyDeferred = any (isJust . feDeferred) itxEntries
k <- insert $ TransactionR c itxDate itxDescr anyDeferred
mapM_ (go k) itxEntries
where
go k tx = do
ek <- insertEntry k tx
case itxCommit of
BudgetCommit _ name -> insert_ $ BudgetLabelR ek name
_ -> return ()
updateTx :: MonadSqlQuery m => UEBalanced -> m ()
updateTx UpdateEntry {ueID, ueValue} = update ueID [EntryRValue =. unStaticValue ueValue]

View File

@ -1,21 +1,13 @@
module Internal.History module Internal.History
( readHistStmt ( readHistStmt
, readHistTransfer , readHistTransfer
, insertAll
, splitHistory , splitHistory
, balanceTxs
, updateTx
, entryPair_
, expandTransfers
, entryPair
) )
where where
import Control.Monad.Except import Control.Monad.Except
import Data.Csv import Data.Csv
import Data.Foldable import Data.Foldable
import Database.Persist ((=.))
import Database.Persist.Monad hiding (get)
import Internal.Database import Internal.Database
import Internal.Types.Main import Internal.Types.Main
import Internal.Utils import Internal.Utils
@ -24,20 +16,32 @@ import qualified RIO.ByteString.Lazy as BL
import RIO.FilePath import RIO.FilePath
import qualified RIO.List as L import qualified RIO.List as L
import qualified RIO.Map as M import qualified RIO.Map as M
import qualified RIO.NonEmpty as NE
import RIO.State
import qualified RIO.Text as T import qualified RIO.Text as T
import RIO.Time import RIO.Time
import qualified RIO.Vector as V import qualified RIO.Vector as V
-- TODO unify this with the transfer system I use in the budget now -- NOTE keep statement and transfer readers separate because the former needs
-- the IO monad, and thus will throw IO errors rather than using the ExceptT
-- thingy
splitHistory :: [History] -> ([PairedTransfer], [Statement])
splitHistory = partitionEithers . fmap go
where
go (HistTransfer x) = Left x
go (HistStatement x) = Right x
--------------------------------------------------------------------------------
-- Transfers
readHistTransfer readHistTransfer
:: (MonadInsertError m, MonadFinance m) :: (MonadInsertError m, MonadFinance m)
=> PairedTransfer => PairedTransfer
-> m (Either CommitR [Tx TxCommit]) -> m (Either CommitR [Tx TxCommit])
readHistTransfer ht = eitherHash CTManual ht return $ \c -> do readHistTransfer ht = eitherHash CTManual ht return $ \c -> do
bounds <- askDBState kmStatementInterval bounds <- askDBState kmStatementInterval
expandTransfer (HistoryCommit c) (Just bounds) ht expandTransfer (HistoryCommit c) bounds ht
--------------------------------------------------------------------------------
-- Statements
readHistStmt readHistStmt
:: (MonadUnliftIO m, MonadFinance m) :: (MonadUnliftIO m, MonadFinance m)
@ -49,172 +53,6 @@ readHistStmt root i = eitherHash CTImport i return $ \c -> do
bounds <- askDBState kmStatementInterval bounds <- askDBState kmStatementInterval
return $ filter (inDaySpan bounds . txDate) $ fmap (\t -> t {txCommit = HistoryCommit c}) bs return $ filter (inDaySpan bounds . txDate) $ fmap (\t -> t {txCommit = HistoryCommit c}) bs
splitHistory :: [History] -> ([PairedTransfer], [Statement])
splitHistory = partitionEithers . fmap go
where
go (HistTransfer x) = Left x
go (HistStatement x) = Right x
insertAll
:: (MonadInsertError m, MonadSqlQuery m, MonadFinance m)
=> [EntryBin]
-> m ()
insertAll ebs = do
(toUpdate, toInsert) <- balanceTxs ebs
mapM_ updateTx toUpdate
forM_ (groupWith itxCommit toInsert) $
\(c, ts) -> do
ck <- insert $ getCommit c
mapM_ (insertTx ck) ts
where
getCommit (HistoryCommit c) = c
getCommit (BudgetCommit c _) = c
insertTx :: MonadSqlQuery m => CommitRId -> InsertTx -> m ()
insertTx c InsertTx {itxDate, itxDescr, itxEntries, itxCommit} = do
let anyDeferred = any (isJust . feDeferred) itxEntries
k <- insert $ TransactionR c itxDate itxDescr anyDeferred
mapM_ (go k) itxEntries
where
go k tx = do
ek <- insertEntry k tx
case itxCommit of
BudgetCommit _ name -> insert_ $ BudgetLabelR ek name
_ -> return ()
updateTx :: MonadSqlQuery m => UEBalanced -> m ()
updateTx UpdateEntry {ueID, ueValue} = update ueID [EntryRValue =. unStaticValue ueValue]
--------------------------------------------------------------------------------
-- low-level transaction stuff
expandTransfers
:: (MonadInsertError m, MonadFinance m)
=> TxCommit
-> Maybe DaySpan
-> [PairedTransfer]
-> m [Tx TxCommit]
expandTransfers tc localInterval ts =
fmap (L.sortOn txDate . concat) $
combineErrors $
fmap (expandTransfer tc localInterval) ts
expandTransfer
:: (MonadInsertError m, MonadFinance m)
=> TxCommit
-> Maybe DaySpan
-> PairedTransfer
-> m [Tx TxCommit]
expandTransfer tc ds Transfer {transAmounts, transTo, transCurrency, transFrom} = do
txs <- concat <$> mapErrors go transAmounts
return $ case ds of
Nothing -> txs
Just bounds -> filter (inDaySpan bounds . txDate) txs
where
go
Amount
{ amtWhen = pat
, amtValue = TransferValue {tvVal = v, tvType = t}
, amtDesc = desc
} =
withDates pat $ \day -> do
p <- entryPair_ (\_ x -> EntryValue t $ toRational x) transFrom transTo transCurrency desc v
return
Tx
{ txCommit = tc
, txDate = day
, txPrimary = p
, txOther = []
, txDescr = desc
}
entryPair
:: (MonadInsertError m, MonadFinance m)
=> TaggedAcnt
-> TaggedAcnt
-> CurID
-> T.Text
-> Double
-> m (EntrySet AcntID CurrencyPrec TagID Rational (EntryValue Rational))
entryPair = entryPair_ (fmap (EntryValue TFixed) . roundPrecisionCur)
entryPair_
:: (MonadInsertError m, MonadFinance m)
=> (CurrencyPrec -> v -> v')
-> TaggedAcnt
-> TaggedAcnt
-> CurID
-> T.Text
-> v
-> m (EntrySet AcntID CurrencyPrec TagID Rational v')
entryPair_ f from to curid com val = do
cp <- lookupCurrency curid
return $ pair cp from to (f cp val)
where
halfEntry :: a -> [t] -> HalfEntrySet a c t v
halfEntry a ts =
HalfEntrySet
{ hesPrimary = Entry {eAcnt = a, eValue = (), eComment = com, eTags = ts}
, hesOther = []
}
pair cp (TaggedAcnt fa fts) (TaggedAcnt ta tts) v =
EntrySet
{ esCurrency = cp
, esTotalValue = v
, esFrom = halfEntry fa fts
, esTo = halfEntry ta tts
}
withDates
:: (MonadFinance m, MonadInsertError m)
=> DatePat
-> (Day -> m a)
-> m [a]
withDates dp f = do
bounds <- askDBState kmBudgetInterval
days <- liftExcept $ expandDatePat bounds dp
combineErrors $ fmap f days
-- -- TODO tags here?
-- txPair
-- :: CommitR
-- -> Day
-- -> AcntID
-- -> AcntID
-- -> CurrencyPrec
-- -> TransferValue
-- -> T.Text
-- -> Tx TxCommit
-- txPair commit day from to cur (TransferValue t v) desc =
-- Tx
-- { txDescr = desc
-- , txDate = day
-- , txCommit = HistoryCommit commit
-- , txPrimary =
-- EntrySet
-- { esTotalValue = EntryValue t $ toRational v
-- , esCurrency = cur
-- , esFrom = HalfEntrySet {hesPrimary = entry from, hesOther = []}
-- , esTo = HalfEntrySet {hesPrimary = entry to, hesOther = []}
-- }
-- , txOther = []
-- }
-- where
-- entry a =
-- Entry
-- { eAcnt = a
-- , eValue = ()
-- , eComment = ""
-- , eTags = []
-- }
-- resolveTx :: (MonadInsertError m, MonadFinance m) => BalTx CommitR -> m (KeyTx CommitR)
-- resolveTx t@Tx {txEntries = ss} =
-- (\kss -> t {txEntries = kss}) <$> mapErrors resolveEntry ss
--------------------------------------------------------------------------------
-- Statements
-- TODO this probably won't scale well (pipes?) -- TODO this probably won't scale well (pipes?)
readImport :: (MonadUnliftIO m, MonadFinance m) => FilePath -> Statement -> m [Tx ()] readImport :: (MonadUnliftIO m, MonadFinance m) => FilePath -> Statement -> m [Tx ()]
readImport root Statement {stmtTxOpts, stmtParsers, stmtSkipLines, stmtDelim, stmtPaths} = do readImport root Statement {stmtTxOpts, stmtParsers, stmtSkipLines, stmtDelim, stmtPaths} = do
@ -423,279 +261,3 @@ matchNonDates ms = go ([], [], initZipper ms)
MatchSkip -> (Nothing : matched, unmatched) MatchSkip -> (Nothing : matched, unmatched)
MatchFail -> (matched, r : unmatched) MatchFail -> (matched, r : unmatched)
in go (m, u, resetZipper z') rs in go (m, u, resetZipper z') rs
balanceTxs
:: (MonadInsertError m, MonadFinance m)
=> [EntryBin]
-> m ([UEBalanced], [InsertTx])
balanceTxs ebs =
first concat . partitionEithers . catMaybes
<$> evalStateT (mapErrors go $ L.sortOn binDate ebs) M.empty
where
go (ToUpdate utx) = fmap (Just . Left) $ liftInnerS $ rebalanceEntrySet utx
go (ToRead ReadEntry {reCurrency, reAcnt, reValue}) = do
modify $ mapAdd_ (reAcnt, reCurrency) reValue
return Nothing
go (ToInsert Tx {txPrimary, txOther, txDescr, txCommit, txDate}) = do
e <- balanceEntrySet primaryBalance txPrimary
-- TODO this logic is really stupid, I'm balancing the total twice; fix
-- will likely entail making a separate data structure for txs derived
-- from transfers vs statements
let etot = sum $ eValue . feEntry <$> filter ((< 0) . feIndex) e
es <- mapErrors (balanceEntrySet (secondaryBalance etot)) txOther
let tx =
InsertTx
{ itxDescr = txDescr
, itxDate = txDate
, itxEntries = concat $ e : es
, itxCommit = txCommit
}
return $ Just $ Right tx
primaryBalance Entry {eAcnt} c (EntryValue t v) = findBalance eAcnt c t v
secondaryBalance tot Entry {eAcnt} c val = case val of
Right (EntryValue t v) -> findBalance eAcnt c t v
Left v -> return $ toRational v * tot
binDate :: EntryBin -> Day
binDate (ToUpdate UpdateEntrySet {utDate}) = utDate
binDate (ToRead ReadEntry {reDate}) = reDate
binDate (ToInsert Tx {txDate}) = txDate
type EntryBals = M.Map (AccountRId, CurrencyRId) Rational
data UpdateEntryType a
= UET_ReadOnly UE_RO
| UET_Unk UEUnk
| UET_Linked a
-- TODO make sure new values are rounded properly here
rebalanceEntrySet :: UpdateEntrySet -> State EntryBals [UEBalanced]
rebalanceEntrySet
UpdateEntrySet
{ utFrom0
, utTo0
, utPairs
, utFromUnk
, utToUnk
, utFromRO
, utToRO
, utCurrency
, utToUnkLink0
, utTotalValue
} =
do
(f0val, (tpairs, fs)) <-
fmap (second partitionEithers) $
foldM goFrom (utTotalValue, []) $
L.sortOn idx $
(UET_ReadOnly <$> utFromRO)
++ (UET_Unk <$> utFromUnk)
++ (UET_Linked <$> utPairs)
let f0 = utFrom0 {ueValue = StaticValue f0val}
let tsLink0 = fmap (unlink (-f0val)) utToUnkLink0
(t0val, tsUnk) <-
fmap (second catMaybes) $
foldM goTo (-utTotalValue, []) $
L.sortOn idx2 $
(UET_Linked <$> (tpairs ++ tsLink0))
++ (UET_Unk <$> utToUnk)
++ (UET_ReadOnly <$> utToRO)
let t0 = utTo0 {ueValue = StaticValue t0val}
return (f0 : fs ++ (t0 : tsUnk))
where
project f _ _ (UET_ReadOnly e) = f e
project _ f _ (UET_Unk e) = f e
project _ _ f (UET_Linked p) = f p
idx = project ueIndex ueIndex (ueIndex . fst)
idx2 = project ueIndex ueIndex ueIndex
-- TODO the sum accumulator thing is kinda awkward
goFrom (tot, es) (UET_ReadOnly e) = do
v <- updateFixed e
return (tot - v, es)
goFrom (tot, esPrev) (UET_Unk e) = do
v <- updateUnknown e
return (tot - v, Right e {ueValue = StaticValue v} : esPrev)
goFrom (tot, esPrev) (UET_Linked (e0, es)) = do
v <- updateUnknown e0
let e0' = Right $ e0 {ueValue = StaticValue v}
let es' = fmap (Left . unlink (-v)) es
return (tot - v, (e0' : es') ++ esPrev)
goTo (tot, esPrev) (UET_ReadOnly e) = do
v <- updateFixed e
return (tot - v, esPrev)
goTo (tot, esPrev) (UET_Linked e) = do
v <- updateFixed e
return (tot - v, Just e : esPrev)
goTo (tot, esPrev) (UET_Unk e) = do
v <- updateUnknown e
return (tot - v, Just e {ueValue = StaticValue v} : esPrev)
updateFixed :: UpdateEntry i StaticValue -> State EntryBals Rational
updateFixed e = do
let v = unStaticValue $ ueValue e
modify $ mapAdd_ (ueAcnt e, utCurrency) v
return v
updateUnknown e = do
let key = (ueAcnt e, utCurrency)
curBal <- gets (M.findWithDefault 0 key)
let v = case ueValue e of
EVPercent p -> p * curBal
EVBalance p -> p - curBal
modify $ mapAdd_ key v
return v
unlink v e = e {ueValue = StaticValue $ v * unLinkScale (ueValue e)}
balanceEntrySet
:: (MonadInsertError m, MonadFinance m)
=> (Entry AccountRId AcntSign TagRId -> CurrencyRId -> v -> State EntryBals Rational)
-> DeferredEntrySet v
-> StateT EntryBals m [KeyEntry]
balanceEntrySet
findTot
EntrySet
{ esFrom = HalfEntrySet {hesPrimary = f0, hesOther = fs}
, esTo = HalfEntrySet {hesPrimary = t0, hesOther = ts}
, esCurrency = CurrencyPrec {cpID = curID, cpPrec = precision}
, esTotalValue
} =
do
-- 1. Resolve tag and accout ids in primary entries since we (might) need
-- them later to calculate the total value of the transaction.
let f0res = resolveAcntAndTags f0
let t0res = resolveAcntAndTags t0
combineErrorM f0res t0res $ \f0' t0' -> do
-- 2. Compute total value of transaction using the primary debit entry
tot <- liftInnerS $ findTot f0' curID esTotalValue
-- 3. Balance all debit entries (including primary). Note the negative
-- indices, which will signify them to be debit entries when updated
-- later.
let balFromEntry = balanceEntry (\a -> liftInnerS . balanceDeferred curID a) curID
fs' <- doEntries balFromEntry curID tot f0' fs (NE.iterate (+ (-1)) (-1))
-- 4. Build an array of debit values be linked as desired in credit entries
let fv = V.fromList $ fmap (eValue . feEntry) fs'
-- 4. Balance credit entries (including primary) analogously.
let balToEntry = balanceEntry (balanceLinked fv curID precision) curID
ts' <- doEntries balToEntry curID (-tot) t0' ts (NE.iterate (+ 1) 0)
return $ fs' ++ ts'
doEntries
:: (MonadInsertError m)
=> (Int -> Entry AcntID v TagID -> StateT EntryBals m (InsertEntry AccountRId CurrencyRId TagRId))
-> CurrencyRId
-> Rational
-> Entry AccountRId AcntSign TagRId
-> [Entry AcntID v TagID]
-> NonEmpty Int
-> StateT EntryBals m [InsertEntry AccountRId CurrencyRId TagRId]
doEntries f curID tot e es (i0 :| iN) = do
es' <- mapErrors (uncurry f) $ zip iN es
let e0val = tot - entrySum es'
-- TODO not dry
let s = fromIntegral $ sign2Int (eValue e) -- NOTE hack
modify (mapAdd_ (eAcnt e, curID) tot)
let e' =
InsertEntry
{ feEntry = e {eValue = s * e0val}
, feCurrency = curID
, feDeferred = Nothing
, feIndex = i0
}
return $ e' : es'
where
entrySum = sum . fmap (eValue . feEntry)
liftInnerS :: Monad m => StateT e Identity a -> StateT e m a
liftInnerS = mapStateT (return . runIdentity)
balanceLinked
:: MonadInsertError m
=> Vector Rational
-> CurrencyRId
-> Natural
-> AccountRId
-> LinkDeferred Rational
-> StateT EntryBals m (Rational, Maybe DBDeferred)
balanceLinked from curID precision acntID lg = case lg of
(LinkIndex LinkedNumGetter {lngIndex, lngScale}) -> do
let res = fmap (go lngScale) $ from V.!? fromIntegral lngIndex
case res of
Just v -> return (v, Just $ EntryLinked lngIndex $ toRational lngScale)
-- TODO this error would be much more informative if I had access to the
-- file from which it came
Nothing -> throwError undefined
(LinkDeferred d) -> liftInnerS $ balanceDeferred curID acntID d
where
go s = roundPrecision precision . (* s) . fromRational
balanceDeferred
:: CurrencyRId
-> AccountRId
-> EntryValue Rational
-> State EntryBals (Rational, Maybe DBDeferred)
balanceDeferred curID acntID (EntryValue t v) = do
newval <- findBalance acntID curID t v
let d = case t of
TFixed -> Nothing
TBalance -> Just $ EntryBalance v
TPercent -> Just $ EntryPercent v
return (newval, d)
balanceEntry
:: (MonadInsertError m, MonadFinance m)
=> (AccountRId -> v -> StateT EntryBals m (Rational, Maybe DBDeferred))
-> CurrencyRId
-> Int
-> Entry AcntID v TagID
-> StateT EntryBals m (InsertEntry AccountRId CurrencyRId TagRId)
balanceEntry f curID idx e@Entry {eValue, eAcnt, eTags} = do
let acntRes = lookupAccount eAcnt
let tagRes = mapErrors lookupTag eTags
combineErrorM acntRes tagRes $ \(acntID, sign, _) tags -> do
let s = fromIntegral $ sign2Int sign
(newVal, deferred) <- f acntID eValue
modify (mapAdd_ (acntID, curID) newVal)
return $
InsertEntry
{ feEntry = e {eValue = s * newVal, eAcnt = acntID, eTags = tags}
, feCurrency = curID
, feDeferred = deferred
, feIndex = idx
}
resolveAcntAndTags
:: (MonadInsertError m, MonadFinance m)
=> Entry AcntID v TagID
-> m (Entry AccountRId AcntSign TagRId)
resolveAcntAndTags e@Entry {eAcnt, eTags} = do
let acntRes = lookupAccount eAcnt
let tagRes = mapErrors lookupTag eTags
-- TODO total hack, store account sign in the value field so I don't need to
-- make seperate tuple pair thing to haul it around. Weird, but it works.
combineError acntRes tagRes $
\(acntID, sign, _) tags -> e {eAcnt = acntID, eTags = tags, eValue = sign}
findBalance
:: AccountRId
-> CurrencyRId
-> TransferType
-> Rational
-> State EntryBals Rational
findBalance acnt cur t v = do
curBal <- gets (M.findWithDefault 0 (acnt, cur))
return $ case t of
TBalance -> v - curBal
TPercent -> v * curBal
TFixed -> v
-- -- reimplementation from future version :/
-- mapAccumM
-- :: Monad m
-- => (s -> a -> m (s, b))
-- -> s
-- -> [a]
-- -> m (s, [b])
-- mapAccumM f s xs = foldrM go (s, []) xs
-- where
-- go x (s', acc) = second (: acc) <$> f s' x

View File

@ -60,6 +60,11 @@ module Internal.Utils
, mapAdd_ , mapAdd_
, groupKey , groupKey
, groupWith , groupWith
, balanceTxs
, expandTransfers
, expandTransfer
, entryPair
, entryPair_
) )
where where
@ -72,8 +77,10 @@ import RIO
import qualified RIO.List as L import qualified RIO.List as L
import qualified RIO.Map as M import qualified RIO.Map as M
import qualified RIO.NonEmpty as NE import qualified RIO.NonEmpty as NE
import RIO.State
import qualified RIO.Text as T import qualified RIO.Text as T
import RIO.Time import RIO.Time
import qualified RIO.Vector as V
import Text.Regex.TDFA import Text.Regex.TDFA
import Text.Regex.TDFA.Text import Text.Regex.TDFA.Text
@ -1021,3 +1028,350 @@ lookupFinance
-> T.Text -> T.Text
-> m a -> m a
lookupFinance t f c = (liftExcept . lookupErr (DBKey t) c) =<< askDBState f lookupFinance t f c = (liftExcept . lookupErr (DBKey t) c) =<< askDBState f
balanceTxs
:: (MonadInsertError m, MonadFinance m)
=> [EntryBin]
-> m ([UEBalanced], [InsertTx])
balanceTxs ebs =
first concat . partitionEithers . catMaybes
<$> evalStateT (mapErrors go $ L.sortOn binDate ebs) M.empty
where
go (ToUpdate utx) = fmap (Just . Left) $ liftInnerS $ rebalanceEntrySet utx
go (ToRead ReadEntry {reCurrency, reAcnt, reValue}) = do
modify $ mapAdd_ (reAcnt, reCurrency) reValue
return Nothing
go (ToInsert Tx {txPrimary, txOther, txDescr, txCommit, txDate}) = do
e <- balanceEntrySet primaryBalance txPrimary
-- TODO this logic is really stupid, I'm balancing the total twice; fix
-- will likely entail making a separate data structure for txs derived
-- from transfers vs statements
let etot = sum $ eValue . feEntry <$> filter ((< 0) . feIndex) e
es <- mapErrors (balanceEntrySet (secondaryBalance etot)) txOther
let tx =
InsertTx
{ itxDescr = txDescr
, itxDate = txDate
, itxEntries = concat $ e : es
, itxCommit = txCommit
}
return $ Just $ Right tx
primaryBalance Entry {eAcnt} c (EntryValue t v) = findBalance eAcnt c t v
secondaryBalance tot Entry {eAcnt} c val = case val of
Right (EntryValue t v) -> findBalance eAcnt c t v
Left v -> return $ toRational v * tot
binDate :: EntryBin -> Day
binDate (ToUpdate UpdateEntrySet {utDate}) = utDate
binDate (ToRead ReadEntry {reDate}) = reDate
binDate (ToInsert Tx {txDate}) = txDate
type EntryBals = M.Map (AccountRId, CurrencyRId) Rational
data UpdateEntryType a
= UET_ReadOnly UE_RO
| UET_Unk UEUnk
| UET_Linked a
-- TODO make sure new values are rounded properly here
rebalanceEntrySet :: UpdateEntrySet -> State EntryBals [UEBalanced]
rebalanceEntrySet
UpdateEntrySet
{ utFrom0
, utTo0
, utPairs
, utFromUnk
, utToUnk
, utFromRO
, utToRO
, utCurrency
, utToUnkLink0
, utTotalValue
} =
do
(f0val, (tpairs, fs)) <-
fmap (second partitionEithers) $
foldM goFrom (utTotalValue, []) $
L.sortOn idx $
(UET_ReadOnly <$> utFromRO)
++ (UET_Unk <$> utFromUnk)
++ (UET_Linked <$> utPairs)
let f0 = utFrom0 {ueValue = StaticValue f0val}
let tsLink0 = fmap (unlink (-f0val)) utToUnkLink0
(t0val, tsUnk) <-
fmap (second catMaybes) $
foldM goTo (-utTotalValue, []) $
L.sortOn idx2 $
(UET_Linked <$> (tpairs ++ tsLink0))
++ (UET_Unk <$> utToUnk)
++ (UET_ReadOnly <$> utToRO)
let t0 = utTo0 {ueValue = StaticValue t0val}
return (f0 : fs ++ (t0 : tsUnk))
where
project f _ _ (UET_ReadOnly e) = f e
project _ f _ (UET_Unk e) = f e
project _ _ f (UET_Linked p) = f p
idx = project ueIndex ueIndex (ueIndex . fst)
idx2 = project ueIndex ueIndex ueIndex
-- TODO the sum accumulator thing is kinda awkward
goFrom (tot, es) (UET_ReadOnly e) = do
v <- updateFixed e
return (tot - v, es)
goFrom (tot, esPrev) (UET_Unk e) = do
v <- updateUnknown e
return (tot - v, Right e {ueValue = StaticValue v} : esPrev)
goFrom (tot, esPrev) (UET_Linked (e0, es)) = do
v <- updateUnknown e0
let e0' = Right $ e0 {ueValue = StaticValue v}
let es' = fmap (Left . unlink (-v)) es
return (tot - v, (e0' : es') ++ esPrev)
goTo (tot, esPrev) (UET_ReadOnly e) = do
v <- updateFixed e
return (tot - v, esPrev)
goTo (tot, esPrev) (UET_Linked e) = do
v <- updateFixed e
return (tot - v, Just e : esPrev)
goTo (tot, esPrev) (UET_Unk e) = do
v <- updateUnknown e
return (tot - v, Just e {ueValue = StaticValue v} : esPrev)
updateFixed :: UpdateEntry i StaticValue -> State EntryBals Rational
updateFixed e = do
let v = unStaticValue $ ueValue e
modify $ mapAdd_ (ueAcnt e, utCurrency) v
return v
updateUnknown e = do
let key = (ueAcnt e, utCurrency)
curBal <- gets (M.findWithDefault 0 key)
let v = case ueValue e of
EVPercent p -> p * curBal
EVBalance p -> p - curBal
modify $ mapAdd_ key v
return v
unlink v e = e {ueValue = StaticValue $ v * unLinkScale (ueValue e)}
balanceEntrySet
:: (MonadInsertError m, MonadFinance m)
=> (Entry AccountRId AcntSign TagRId -> CurrencyRId -> v -> State EntryBals Rational)
-> DeferredEntrySet v
-> StateT EntryBals m [KeyEntry]
balanceEntrySet
findTot
EntrySet
{ esFrom = HalfEntrySet {hesPrimary = f0, hesOther = fs}
, esTo = HalfEntrySet {hesPrimary = t0, hesOther = ts}
, esCurrency = CurrencyPrec {cpID = curID, cpPrec = precision}
, esTotalValue
} =
do
-- 1. Resolve tag and accout ids in primary entries since we (might) need
-- them later to calculate the total value of the transaction.
let f0res = resolveAcntAndTags f0
let t0res = resolveAcntAndTags t0
combineErrorM f0res t0res $ \f0' t0' -> do
-- 2. Compute total value of transaction using the primary debit entry
tot <- liftInnerS $ findTot f0' curID esTotalValue
-- 3. Balance all debit entries (including primary). Note the negative
-- indices, which will signify them to be debit entries when updated
-- later.
let balFromEntry = balanceEntry (\a -> liftInnerS . balanceDeferred curID a) curID
fs' <- doEntries balFromEntry curID tot f0' fs (NE.iterate (+ (-1)) (-1))
-- 4. Build an array of debit values be linked as desired in credit entries
let fv = V.fromList $ fmap (eValue . feEntry) fs'
-- 4. Balance credit entries (including primary) analogously.
let balToEntry = balanceEntry (balanceLinked fv curID precision) curID
ts' <- doEntries balToEntry curID (-tot) t0' ts (NE.iterate (+ 1) 0)
return $ fs' ++ ts'
doEntries
:: (MonadInsertError m)
=> (Int -> Entry AcntID v TagID -> StateT EntryBals m (InsertEntry AccountRId CurrencyRId TagRId))
-> CurrencyRId
-> Rational
-> Entry AccountRId AcntSign TagRId
-> [Entry AcntID v TagID]
-> NonEmpty Int
-> StateT EntryBals m [InsertEntry AccountRId CurrencyRId TagRId]
doEntries f curID tot e es (i0 :| iN) = do
es' <- mapErrors (uncurry f) $ zip iN es
let e0val = tot - entrySum es'
-- TODO not dry
let s = fromIntegral $ sign2Int (eValue e) -- NOTE hack
modify (mapAdd_ (eAcnt e, curID) tot)
let e' =
InsertEntry
{ feEntry = e {eValue = s * e0val}
, feCurrency = curID
, feDeferred = Nothing
, feIndex = i0
}
return $ e' : es'
where
entrySum = sum . fmap (eValue . feEntry)
liftInnerS :: Monad m => StateT e Identity a -> StateT e m a
liftInnerS = mapStateT (return . runIdentity)
balanceLinked
:: MonadInsertError m
=> Vector Rational
-> CurrencyRId
-> Natural
-> AccountRId
-> LinkDeferred Rational
-> StateT EntryBals m (Rational, Maybe DBDeferred)
balanceLinked from curID precision acntID lg = case lg of
(LinkIndex LinkedNumGetter {lngIndex, lngScale}) -> do
let res = fmap (go lngScale) $ from V.!? fromIntegral lngIndex
case res of
Just v -> return (v, Just $ EntryLinked lngIndex $ toRational lngScale)
-- TODO this error would be much more informative if I had access to the
-- file from which it came
Nothing -> throwError undefined
(LinkDeferred d) -> liftInnerS $ balanceDeferred curID acntID d
where
go s = roundPrecision precision . (* s) . fromRational
balanceDeferred
:: CurrencyRId
-> AccountRId
-> EntryValue Rational
-> State EntryBals (Rational, Maybe DBDeferred)
balanceDeferred curID acntID (EntryValue t v) = do
newval <- findBalance acntID curID t v
let d = case t of
TFixed -> Nothing
TBalance -> Just $ EntryBalance v
TPercent -> Just $ EntryPercent v
return (newval, d)
balanceEntry
:: (MonadInsertError m, MonadFinance m)
=> (AccountRId -> v -> StateT EntryBals m (Rational, Maybe DBDeferred))
-> CurrencyRId
-> Int
-> Entry AcntID v TagID
-> StateT EntryBals m (InsertEntry AccountRId CurrencyRId TagRId)
balanceEntry f curID idx e@Entry {eValue, eAcnt, eTags} = do
let acntRes = lookupAccount eAcnt
let tagRes = mapErrors lookupTag eTags
combineErrorM acntRes tagRes $ \(acntID, sign, _) tags -> do
let s = fromIntegral $ sign2Int sign
(newVal, deferred) <- f acntID eValue
modify (mapAdd_ (acntID, curID) newVal)
return $
InsertEntry
{ feEntry = e {eValue = s * newVal, eAcnt = acntID, eTags = tags}
, feCurrency = curID
, feDeferred = deferred
, feIndex = idx
}
resolveAcntAndTags
:: (MonadInsertError m, MonadFinance m)
=> Entry AcntID v TagID
-> m (Entry AccountRId AcntSign TagRId)
resolveAcntAndTags e@Entry {eAcnt, eTags} = do
let acntRes = lookupAccount eAcnt
let tagRes = mapErrors lookupTag eTags
-- TODO total hack, store account sign in the value field so I don't need to
-- make seperate tuple pair thing to haul it around. Weird, but it works.
combineError acntRes tagRes $
\(acntID, sign, _) tags -> e {eAcnt = acntID, eTags = tags, eValue = sign}
findBalance
:: AccountRId
-> CurrencyRId
-> TransferType
-> Rational
-> State EntryBals Rational
findBalance acnt cur t v = do
curBal <- gets (M.findWithDefault 0 (acnt, cur))
return $ case t of
TBalance -> v - curBal
TPercent -> v * curBal
TFixed -> v
expandTransfers
:: (MonadInsertError m, MonadFinance m)
=> TxCommit
-> DaySpan
-> [PairedTransfer]
-> m [Tx TxCommit]
expandTransfers tc bounds = fmap concat . mapErrors (expandTransfer tc bounds)
expandTransfer
:: (MonadInsertError m, MonadFinance m)
=> TxCommit
-> DaySpan
-> PairedTransfer
-> m [Tx TxCommit]
expandTransfer tc bounds Transfer {transAmounts, transTo, transCurrency, transFrom} = do
txs <- mapErrors go transAmounts
return $ filter (inDaySpan bounds . txDate) $ concat txs
where
go
Amount
{ amtWhen = pat
, amtValue = TransferValue {tvVal = v, tvType = t}
, amtDesc = desc
} =
withDates pat $ \day -> do
p <- entryPair_ (\_ x -> EntryValue t $ toRational x) transFrom transTo transCurrency desc v
return
Tx
{ txCommit = tc
, txDate = day
, txPrimary = p
, txOther = []
, txDescr = desc
}
entryPair
:: (MonadInsertError m, MonadFinance m)
=> TaggedAcnt
-> TaggedAcnt
-> CurID
-> T.Text
-> Double
-> m (EntrySet AcntID CurrencyPrec TagID Rational (EntryValue Rational))
entryPair = entryPair_ (fmap (EntryValue TFixed) . roundPrecisionCur)
entryPair_
:: (MonadInsertError m, MonadFinance m)
=> (CurrencyPrec -> v -> v')
-> TaggedAcnt
-> TaggedAcnt
-> CurID
-> T.Text
-> v
-> m (EntrySet AcntID CurrencyPrec TagID Rational v')
entryPair_ f from to_ curid com val = do
cp <- lookupCurrency curid
return $ pair cp from to_ (f cp val)
where
halfEntry :: a -> [t] -> HalfEntrySet a c t v
halfEntry a ts =
HalfEntrySet
{ hesPrimary = Entry {eAcnt = a, eValue = (), eComment = com, eTags = ts}
, hesOther = []
}
pair cp (TaggedAcnt fa fts) (TaggedAcnt ta tts) v =
EntrySet
{ esCurrency = cp
, esTotalValue = v
, esFrom = halfEntry fa fts
, esTo = halfEntry ta tts
}
withDates
:: (MonadFinance m, MonadInsertError m)
=> DatePat
-> (Day -> m a)
-> m [a]
withDates dp f = do
bounds <- askDBState kmBudgetInterval
days <- liftExcept $ expandDatePat bounds dp
combineErrors $ fmap f days