pwncash/lib/Internal/Insert.hs

402 lines
12 KiB
Haskell
Raw Normal View History

{-# LANGUAGE GADTs #-}
2022-12-11 17:51:11 -05:00
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
2023-01-28 22:55:07 -05:00
{-# LANGUAGE NoImplicitPrelude #-}
2022-12-11 17:51:11 -05:00
module Internal.Insert
( insertStatements
, insertBudget
)
where
import Data.Hashable
import Database.Persist.Class
import Database.Persist.Sql hiding (Single, Statement)
import Internal.Database.Model
import Internal.Statement
import Internal.Types hiding (sign)
import Internal.Utils
import RIO hiding (to)
import qualified RIO.Text as T
import RIO.Time
2022-12-11 17:51:11 -05:00
--------------------------------------------------------------------------------
-- intervals
2023-01-28 19:32:56 -05:00
expandDatePat :: Bounds -> DatePat -> [Day]
expandDatePat (a, b) (Cron cp) = filter (cronPatternMatches cp) [a .. b]
expandDatePat i (Mod mp) = expandModPat mp i
2022-12-11 17:51:11 -05:00
2023-01-28 19:32:56 -05:00
expandModPat :: ModPat -> Bounds -> [Day]
expandModPat
2023-01-25 23:04:54 -05:00
ModPat {mpStart = s, mpBy = b, mpUnit = u, mpRepeats = r}
2023-01-28 19:32:56 -05:00
(lower, upper) =
takeWhile (<= upper) $
(`addFun` start) . (* b')
<$> maybe id (take . fromIntegral) r [0 ..]
where
2023-01-28 19:32:56 -05:00
start = maybe lower fromGregorian' s
b' = fromIntegral b
addFun = case u of
Day -> addDays
Week -> addDays . (* 7)
Month -> addGregorianMonthsClip
Year -> addGregorianYearsClip
2022-12-11 17:51:11 -05:00
-- TODO this can be optimized to prevent filtering a bunch of dates for
-- one/a few cron patterns
2022-12-11 17:51:11 -05:00
cronPatternMatches :: CronPat -> Day -> Bool
cronPatternMatches
CronPat
{ cronWeekly = w
, cronYear = y
, cronMonth = m
, cronDay = d
}
x =
2023-01-29 11:35:30 -05:00
yMaybe y' y && mdMaybe m' m && mdMaybe d' d && wdMaybe (dayOfWeek_ x) w
where
testMaybe = maybe True
2023-01-29 10:51:03 -05:00
yMaybe z = testMaybe (mdyPatternMatches (fromIntegral z))
mdMaybe z = testMaybe (mdyPatternMatches (fromIntegral z))
wdMaybe z = testMaybe (`weekdayPatternMatches` z)
(y', m', d') = toGregorian x
2022-12-11 17:51:11 -05:00
2022-12-14 23:59:23 -05:00
dayOfWeek_ :: Day -> Weekday
dayOfWeek_ d = case dayOfWeek d of
Monday -> Mon
Tuesday -> Tue
2022-12-14 23:59:23 -05:00
Wednesday -> Wed
Thursday -> Thu
Friday -> Fri
Saturday -> Sat
Sunday -> Sun
2022-12-14 23:59:23 -05:00
weekdayPatternMatches :: WeekdayPat -> Weekday -> Bool
weekdayPatternMatches (OnDay x) = (== x)
2022-12-11 17:51:11 -05:00
weekdayPatternMatches (OnDays xs) = (`elem` xs)
2023-01-29 10:51:03 -05:00
mdyPatternMatches :: Natural -> MDYPat -> Bool
mdyPatternMatches x p = case p of
Single y -> x == y
Multi xs -> x `elem` xs
Repeat (RepeatPat {rpStart = s, rpBy = b, rpRepeats = r}) ->
2023-01-29 10:51:03 -05:00
s >= x && mod x b == 0 && maybe True (\y -> x <= s + b * y) r
2022-12-11 17:51:11 -05:00
--------------------------------------------------------------------------------
-- budget
2023-01-25 23:04:54 -05:00
insertBudget :: MonadUnliftIO m => Budget -> MappingT m [InsertError]
insertBudget Budget {income = is, expenses = es} = do
2023-01-28 22:55:07 -05:00
es1 <- mapM insertIncome is
2023-01-30 21:12:08 -05:00
es2 <- mapM insertTransfer es
2023-01-28 22:55:07 -05:00
return $ concat $ es1 ++ es2
2022-12-11 17:51:11 -05:00
-- TODO this hashes twice (not that it really matters)
whenHash
2023-01-24 23:24:41 -05:00
:: (Hashable a, MonadUnliftIO m)
=> ConfigType
-> a
2023-01-24 23:24:41 -05:00
-> b
-> (Key CommitR -> MappingT m b)
-> MappingT m b
whenHash t o def f = do
2022-12-11 17:51:11 -05:00
let h = hash o
hs <- asks kmNewCommits
2023-01-24 23:24:41 -05:00
if h `elem` hs then f =<< lift (insert $ CommitR h t) else return def
2022-12-11 17:51:11 -05:00
-- TODO allow currency conversions here
data BudgetSplit b = BudgetSplit
{ bsAcnt :: AcntID
, bsBucket :: Maybe b
}
data BudgetMeta = BudgetMeta
{ bmCommit :: Key CommitR
, bmWhen :: Day
, bmCur :: CurID
}
data BudgetTx = BudgetTx
{ btMeta :: BudgetMeta
, btFrom :: BudgetSplit IncomeBucket
, btTo :: BudgetSplit ExpenseBucket
, btValue :: Rational
, btDesc :: T.Text
}
2023-01-27 20:31:13 -05:00
insertIncome :: MonadUnliftIO m => Income -> MappingT m [InsertError]
insertIncome i@Income {..} =
whenHash CTIncome i [] $ \c ->
unlessLeft (balanceIncome i) $ \balance ->
fmap concat $ withDates incWhen $ \day -> do
let meta = BudgetMeta c day incCurrency
let fromAllos b = concatMap (fromAllo meta incFrom (Just b))
let pre = fromAllos PreTax incPretax
let tax = fmap (fromTax meta incFrom) incTaxes
let post = fromAllos PostTax incPosttax
let bal =
BudgetTx
{ btMeta = meta
, btFrom = BudgetSplit incFrom $ Just PostTax
, btTo = BudgetSplit incToBal Nothing
, btValue = balance
, btDesc = "balance after deductions"
}
fmap concat $ mapM insertBudgetTx $ bal : (pre ++ tax ++ post)
fromAllo
:: BudgetMeta
-> AcntID
-> Maybe IncomeBucket
-> Allocation
-> [BudgetTx]
fromAllo meta from ib Allocation {..} = fmap (toBT alloPath) alloAmts
where
toBT to (Amount desc v) =
BudgetTx
{ btFrom = BudgetSplit from ib
, btTo = BudgetSplit to $ Just alloBucket
, btValue = dec2Rat v
, btDesc = desc
, btMeta = meta
}
2023-01-27 20:31:13 -05:00
fromTax :: BudgetMeta -> AcntID -> Tax -> BudgetTx
fromTax meta from Tax {taxAcnt = to, taxValue = v} =
BudgetTx
{ btFrom = BudgetSplit from (Just IntraTax)
, btTo = BudgetSplit to (Just Fixed)
, btValue = dec2Rat v
, btDesc = ""
, btMeta = meta
}
withDates
:: MonadUnliftIO m
=> DatePat
-> (Day -> MappingT m a)
-> MappingT m [a]
withDates dp f = do
bounds <- askBounds
mapM f (expandDatePat bounds dp)
askBounds :: MonadUnliftIO m => MappingT m Bounds
askBounds = (liftIO . resolveBounds) =<< asks kmBudgetInterval
balanceIncome :: Income -> EitherErr Rational
balanceIncome
Income
{ incGross = g
2023-01-28 19:32:56 -05:00
, incWhen = dp
, incPretax = pre
, incTaxes = tax
, incPosttax = post
}
2023-01-30 21:12:08 -05:00
| bal < 0 = Left $ IncomeError dp
| otherwise = Right bal
where
bal = dec2Rat g - sum (sumAllocation <$> pre ++ post) - sumTaxes tax
2022-12-11 17:51:11 -05:00
sumAllocation :: Allocation -> Rational
sumAllocation = sum . fmap (dec2Rat . amtValue) . alloAmts
2022-12-11 17:51:11 -05:00
sumTaxes :: [Tax] -> Rational
sumTaxes = sum . fmap (dec2Rat . taxValue)
2023-01-30 21:12:08 -05:00
insertTransfer :: MonadUnliftIO m => Transfer -> MappingT m [InsertError]
insertTransfer t@Transfer {..} =
fmap (concat . concat) $ whenHash CTExpense t [] $ \key -> do
forM transAmounts $ \(TimeAmount amt pat) ->
withDates pat $ \day -> insertBudgetTx $ budgetTx amt day key
where
2023-01-30 21:12:08 -05:00
meta d c = BudgetMeta {bmWhen = d, bmCur = transCurrency, bmCommit = c}
budgetTx (Amount desc v) d c =
BudgetTx
{ btMeta = meta d c
2023-01-30 21:12:08 -05:00
, btFrom = BudgetSplit transFrom Nothing
, btTo = BudgetSplit transTo Nothing
, btValue = dec2Rat v
, btDesc = desc
}
2022-12-11 17:51:11 -05:00
--------------------------------------------------------------------------------
-- statements
2023-01-25 23:04:54 -05:00
insertStatements :: MonadUnliftIO m => Config -> MappingT m [InsertError]
2023-01-27 20:31:13 -05:00
insertStatements conf = concat <$> mapM insertStatement (statements conf)
2023-01-25 23:04:54 -05:00
2023-01-27 20:31:13 -05:00
insertStatement :: MonadUnliftIO m => Statement -> MappingT m [InsertError]
2023-01-28 22:55:07 -05:00
insertStatement (StmtManual m) = insertManual m
2022-12-11 17:51:11 -05:00
insertStatement (StmtImport i) = insertImport i
2023-01-28 22:55:07 -05:00
insertManual :: MonadUnliftIO m => Manual -> MappingT m [InsertError]
insertManual
m@Manual
{ manualDate = dp
, manualFrom = from
, manualTo = to
, manualValue = v
, manualCurrency = u
, manualDesc = e
} = do
2023-01-28 22:55:07 -05:00
whenHash CTManual m [] $ \c -> do
bounds <- (liftIO . resolveBounds) =<< asks kmStatementInterval
2023-01-28 22:55:07 -05:00
res <- mapM tx $ expandDatePat bounds dp
unlessLefts_ (concatEithersL res) $ lift . mapM_ (insertTx c)
where
tx day = txPair day from to u (dec2Rat v) e
2022-12-11 17:51:11 -05:00
2023-01-27 20:31:13 -05:00
insertImport :: MonadUnliftIO m => Import -> MappingT m [InsertError]
insertImport i = whenHash CTImport i [] $ \c -> do
2022-12-11 17:51:11 -05:00
-- TODO this isn't efficient, the whole file will be read and maybe no
-- transactions will be desired
2023-01-28 22:55:07 -05:00
recoverIO (readImport i) $ \r -> unlessLefts r $ \bs -> do
bounds <- asks kmStatementInterval
res <- mapM resolveTx $ filter (inMaybeBounds bounds . txDate) bs
unlessLefts_ (concatEithersL res) $ lift . mapM_ (insertTx c)
where
recoverIO x rest = do
res <- tryIO x
case res of
Right r -> rest r
-- If file is not found (or something else happens) then collect the
-- error try the remaining imports
Left e -> return [InsertIOError $ showT e]
2022-12-11 17:51:11 -05:00
--------------------------------------------------------------------------------
-- low-level transaction stuff
txPair
2023-01-05 22:23:22 -05:00
:: MonadUnliftIO m
=> Day
-> AcntID
-> AcntID
-> CurID
-> Rational
-> T.Text
2023-01-28 22:55:07 -05:00
-> MappingT m (EitherErrs KeyTx)
2022-12-11 17:51:11 -05:00
txPair day from to cur val desc = resolveTx tx
where
split a v = Split {sAcnt = a, sValue = v, sComment = "", sCurrency = cur}
tx =
Tx
{ txDescr = desc
, txDate = day
, txTags = []
, txSplits = [split from (-val), split to val]
}
2022-12-11 17:51:11 -05:00
splitPair
:: MonadUnliftIO m
=> AcntID
-> AcntID
-> CurID
-> Rational
-> MappingT m (EitherErrs (KeySplit, KeySplit))
splitPair from to cur val = do
s1 <- split from (-val)
s2 <- split to val
return $ concatEithers2 s1 s2 (,)
where
split a v =
resolveSplit $
Split
{ sAcnt = a
, sValue = v
, sComment = ""
, sCurrency = cur
}
-- tx =
-- Tx
-- { txDescr = desc
-- , txDate = day
-- , txTags = []
-- , txSplits = [split from (-val), split to val]
-- }
2023-01-28 22:55:07 -05:00
resolveTx :: MonadUnliftIO m => BalTx -> MappingT m (EitherErrs KeyTx)
resolveTx t@Tx {txSplits = ss} = do
2023-01-28 22:55:07 -05:00
res <- concatEithersL <$> mapM resolveSplit ss
return $ fmap (\kss -> t {txSplits = kss}) res
2022-12-11 17:51:11 -05:00
2023-01-28 22:55:07 -05:00
resolveSplit :: MonadUnliftIO m => BalSplit -> MappingT m (EitherErrs KeySplit)
resolveSplit s@Split {sAcnt = p, sCurrency = c, sValue = v} = do
2022-12-11 17:51:11 -05:00
aid <- lookupAccountKey p
cid <- lookupCurrency c
sign <- lookupAccountSign p
-- TODO correct sign here?
-- TODO lenses would be nice here
2023-01-28 22:55:07 -05:00
return $ concatEither3 aid cid sign $ \aid_ cid_ sign_ ->
s
{ sAcnt = aid_
, sCurrency = cid_
, sValue = v * fromIntegral (sign2Int sign_)
}
-- return $ case (aid, cid, sign) of
-- _ -> Nothing
2022-12-11 17:51:11 -05:00
-- insertTax
-- :: MonadUnliftIO m
-- => AcntID
-- -> CurID
-- -> Day
-- -> Key CommitR
-- -> Tax
-- -> MappingT m [InsertError]
-- insertTax from cur day commit Tax {taxAcnt = to, taxValue = amnt} =
-- insertBudgetValue IntraTax Fixed from to cur commit day "" $ dec2Rat amnt
-- insertAllocation
-- :: MonadUnliftIO m
-- => AcntID
-- -> CurID
-- -> IncomeBucket
-- -> Day
-- -> Key CommitR
-- -> Allocation Decimal
-- -> MappingT m [InsertError]
-- insertAllocation from cur ib d c (Allocation to xb as _) = concat <$> mapM go as
-- where
-- go (Amount v desc) = insertBudgetValue ib xb from to cur c d desc $ dec2Rat v
insertBudgetTx :: MonadUnliftIO m => BudgetTx -> MappingT m [InsertError]
insertBudgetTx BudgetTx {..} = do
res <- splitPair (bsAcnt btFrom) (bsAcnt btTo) (bmCur btMeta) btValue
unlessLefts_ res $ \(sFrom, sTo) -> lift $ do
k <- insert $ TransactionR (bmCommit btMeta) (bmWhen btMeta) btDesc
skFrom <- insertSplit k sFrom
bFrom <- insert $ BudgetLabelR skFrom ""
forM_ (bsBucket btFrom) $ \b ->
insert_ $ IncomeBucketR bFrom $ showT b
skTo <- insertSplit k sTo
bTo <- insert $ BudgetLabelR skTo ""
forM_ (bsBucket btTo) $ \b ->
insert_ $ ExpenseBucketR bTo $ showT b
insertTxBucket :: MonadUnliftIO m => Key CommitR -> KeyTx -> SqlPersistT m ()
insertTxBucket c Tx {txDate = d, txDescr = e, txSplits = ss} = do
k <- insert $ TransactionR c d e
2022-12-11 17:51:11 -05:00
mapM_ (insertSplit k) ss
2023-01-05 22:23:22 -05:00
insertTx :: MonadUnliftIO m => Key CommitR -> KeyTx -> SqlPersistT m ()
insertTx = insertTxBucket
2022-12-11 17:51:11 -05:00
insertSplit :: MonadUnliftIO m => Key TransactionR -> KeySplit -> SqlPersistT m (Key SplitR)
insertSplit t Split {sAcnt = aid, sCurrency = cid, sValue = v, sComment = c} = do
insert $ SplitR t cid aid c v
2023-01-28 22:55:07 -05:00
lookupAccount :: MonadUnliftIO m => AcntID -> MappingT m (EitherErr (Key AccountR, AcntSign))
lookupAccount p = lookupErr (DBKey AcntField) p <$> asks kmAccount
lookupAccountKey :: MonadUnliftIO m => AcntID -> MappingT m (EitherErr (Key AccountR))
lookupAccountKey = fmap (fmap fst) . lookupAccount
lookupAccountSign :: MonadUnliftIO m => AcntID -> MappingT m (EitherErr AcntSign)
lookupAccountSign = fmap (fmap snd) . lookupAccount
lookupCurrency :: MonadUnliftIO m => T.Text -> MappingT m (EitherErr (Key CurrencyR))
lookupCurrency c = lookupErr (DBKey CurField) c <$> asks kmCurrency