2023-01-05 22:16:06 -05:00
|
|
|
{-# LANGUAGE GADTs #-}
|
2022-12-11 17:51:11 -05:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
2023-01-30 20:13:25 -05:00
|
|
|
{-# 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
|
2023-01-05 22:16:06 -05:00
|
|
|
)
|
|
|
|
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)
|
2023-02-01 20:56:29 -05:00
|
|
|
-- import qualified RIO.Map as M
|
2023-02-02 23:18:36 -05:00
|
|
|
|
|
|
|
import qualified RIO.List as L
|
2023-01-05 22:16:06 -05:00
|
|
|
import qualified RIO.Text as T
|
|
|
|
import RIO.Time
|
|
|
|
|
2022-12-11 17:51:11 -05:00
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- intervals
|
|
|
|
|
2023-02-01 20:56:29 -05:00
|
|
|
-- expandDatePat :: MonadUnliftIO m => Bounds -> DatePat -> MappingT m [Day]
|
|
|
|
-- expandDatePat d p = do
|
|
|
|
-- -- TODO crude memoization
|
|
|
|
-- v <- asks kmBoundsCache
|
|
|
|
-- modifyMVar v $ \m -> case M.lookup (d, p) m of
|
|
|
|
-- Just ds -> return (m, ds)
|
|
|
|
-- Nothing -> do
|
|
|
|
-- let res = expandDatePat_ d p
|
|
|
|
-- return (M.insert (d, p) res m, res)
|
|
|
|
|
2023-01-28 19:32:56 -05:00
|
|
|
expandDatePat :: Bounds -> DatePat -> [Day]
|
2023-02-02 23:18:36 -05:00
|
|
|
expandDatePat b (Cron cp) = expandCronPat b cp
|
|
|
|
-- expandDatePat (a, b) (Cron cp) =
|
|
|
|
-- fmap xGregToDay $
|
|
|
|
-- filter (cronPatternMatches cp) $
|
|
|
|
-- take (fromIntegral $ diffDays b a) $
|
|
|
|
-- gregorians a
|
2023-01-05 22:16:06 -05:00
|
|
|
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]
|
2023-01-05 22:16:06 -05:00
|
|
|
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 ..]
|
2023-01-05 22:16:06 -05:00
|
|
|
where
|
2023-01-28 19:32:56 -05:00
|
|
|
start = maybe lower fromGregorian' s
|
2023-01-05 22:16:06 -05:00
|
|
|
b' = fromIntegral b
|
|
|
|
addFun = case u of
|
|
|
|
Day -> addDays
|
|
|
|
Week -> addDays . (* 7)
|
|
|
|
Month -> addGregorianMonthsClip
|
|
|
|
Year -> addGregorianYearsClip
|
2022-12-11 17:51:11 -05:00
|
|
|
|
2023-02-02 23:18:36 -05:00
|
|
|
-- nextXGreg_ :: CronPat -> XGregorian -> XGregorian
|
|
|
|
-- nextXGreg_ c XGregorian {xgYear = y, xgMonth = m, xgDay = d, xgDayOfWeek = w}
|
|
|
|
-- | m == 12 && d == 31 = XGregorian (y + 1) 1 1 w_
|
|
|
|
-- | (m == 2 && (not leap && d == 28 || (leap && d == 29)))
|
|
|
|
-- || (m `elem` [4, 6, 9, 11] && d == 30)
|
|
|
|
-- || (d == 31) =
|
|
|
|
-- XGregorian y (m + 1) 1 w_
|
|
|
|
-- | otherwise = XGregorian y m (d + 1) w_
|
|
|
|
-- where
|
|
|
|
-- -- don't use DayOfWeek from Data.Time since this uses mod (which uses a
|
|
|
|
-- -- division opcode) and thus will be slower than just checking for equality
|
|
|
|
-- -- and adding
|
|
|
|
-- w_ = if w == 6 then 0 else w + 1
|
|
|
|
-- leap = isLeapYear $ fromIntegral y
|
|
|
|
|
|
|
|
monthLength :: (Integral a, Integral b, Integral c) => a -> b -> c
|
|
|
|
monthLength y m
|
|
|
|
| m == 2 && isLeapYear (fromIntegral y) = 29
|
|
|
|
| m == 2 = 28
|
|
|
|
| m `elem` [4, 6, 9, 11] = 30
|
|
|
|
| otherwise = 31
|
|
|
|
|
2022-12-19 23:13:05 -05:00
|
|
|
-- TODO this can be optimized to prevent filtering a bunch of dates for
|
|
|
|
-- one/a few cron patterns
|
2023-02-02 23:18:36 -05:00
|
|
|
-- cronPatternMatches :: CronPat -> XGregorian -> Bool
|
|
|
|
-- cronPatternMatches CronPat {..} XGregorian {..} =
|
|
|
|
-- testYMD xgYear cronYear
|
|
|
|
-- && testYMD xgMonth cronMonth
|
|
|
|
-- && testYMD xgDay cronDay
|
|
|
|
-- && testW (dayOfWeek_ xgDayOfWeek) cronWeekly
|
|
|
|
-- where
|
|
|
|
-- testYMD z = maybe True (mdyPatternMatches (fromIntegral z))
|
|
|
|
-- testW z = maybe True (`weekdayPatternMatches` z)
|
|
|
|
|
|
|
|
expandCronPat :: Bounds -> CronPat -> [Day]
|
|
|
|
expandCronPat b = L.unfoldr nextCronPat . compileCronPat b
|
|
|
|
|
|
|
|
data CompiledCronPat = CompiledCronPat
|
|
|
|
{ ccpYear :: ![Int]
|
|
|
|
, ccpMonth :: !(Zipper Int)
|
|
|
|
, ccpDay :: !(Zipper Int)
|
|
|
|
, ccpWeekly :: ![Int]
|
|
|
|
, ccpMonthEnd :: !Int
|
|
|
|
, ccpDayEnd :: !Int
|
|
|
|
}
|
|
|
|
deriving (Show)
|
|
|
|
|
|
|
|
data Zipper a = Zipper ![a] ![a] deriving (Show)
|
|
|
|
|
|
|
|
initZipper :: [a] -> Zipper a
|
|
|
|
initZipper = Zipper []
|
|
|
|
|
|
|
|
resetZipper :: Zipper a -> Zipper a
|
|
|
|
resetZipper (Zipper bs as) = initZipper $ reverse bs ++ as
|
|
|
|
|
|
|
|
shiftZipperWhile :: (a -> Bool) -> Zipper a -> Zipper a
|
|
|
|
shiftZipperWhile f z@(Zipper bs as) = case as of
|
|
|
|
[] -> z
|
|
|
|
x : xs
|
|
|
|
| f x -> shiftZipperWhile f $ Zipper (x : bs) xs
|
|
|
|
| otherwise -> z
|
|
|
|
|
|
|
|
zipperCurrent :: Zipper a -> Either (Zipper a) (a, Zipper a)
|
|
|
|
zipperCurrent z@(Zipper _ []) = Left $ resetZipper z
|
|
|
|
zipperCurrent (Zipper bs (a : as)) = Right (a, Zipper (a : bs) as)
|
|
|
|
|
|
|
|
compileCronPat :: Bounds -> CronPat -> CompiledCronPat
|
|
|
|
compileCronPat (x, y) CronPat {..} =
|
|
|
|
CompiledCronPat
|
|
|
|
{ ccpYear = maybe [y0_ .. y1_] compileMDY_ cronYear
|
|
|
|
, ccpMonth = compileDY [1 .. 12] m0 cronMonth
|
|
|
|
, ccpDay = compileDY [1 .. 31] d0 cronDay
|
|
|
|
, ccpWeekly = maybe [] compileW cronWeekly
|
|
|
|
, ccpMonthEnd = m1
|
|
|
|
, ccpDayEnd = d1
|
|
|
|
}
|
|
|
|
where
|
|
|
|
(y0, m0, d0) = toGregorian x
|
|
|
|
(y1, m1, d1) = toGregorian y
|
|
|
|
y0_ = fromIntegral y0
|
|
|
|
y1_ = fromIntegral y1
|
|
|
|
compileDY def k = shiftZipperWhile (< k) . initZipper . maybe def compileMDY_
|
|
|
|
compileMDY_ (Single z) = [fromIntegral z]
|
|
|
|
compileMDY_ (Multi zs) = fromIntegral <$> zs
|
|
|
|
compileMDY_ (Repeat RepeatPat {rpStart = s, rpBy = b, rpRepeats = rs}) =
|
|
|
|
-- TODO minor perf improvement, filter the repeats before filterng <=31
|
|
|
|
let b' = fromIntegral b
|
|
|
|
xs = takeWhile (<= 31) $ L.iterate (+ b') $ fromIntegral s
|
|
|
|
in maybe xs (\r -> take (fromIntegral r) xs) rs
|
|
|
|
|
|
|
|
compileW (OnDay w) = [fromEnum w]
|
|
|
|
compileW (OnDays ws) = fromEnum <$> ws
|
|
|
|
|
|
|
|
nextCronPat :: CompiledCronPat -> Maybe (Day, CompiledCronPat)
|
|
|
|
nextCronPat CompiledCronPat {ccpYear = []} = Nothing
|
|
|
|
nextCronPat c@(CompiledCronPat {..}) =
|
|
|
|
case zipperCurrent ccpMonth of
|
|
|
|
Left mz -> nextCronPat $ c {ccpYear = ys, ccpMonth = mz, ccpDay = resetZipper ccpDay}
|
|
|
|
Right (m, mz) -> case zipperCurrent ccpDay of
|
|
|
|
Left dz -> nextCronPat $ c {ccpMonth = mz, ccpDay = dz}
|
|
|
|
Right (d, dz)
|
|
|
|
| null ys && m >= ccpMonthEnd && d >= ccpDayEnd -> Nothing
|
|
|
|
| otherwise -> case dayMaybe m d of
|
|
|
|
Nothing -> nextCronPat $ c {ccpMonth = mz, ccpDay = resetZipper dz}
|
|
|
|
Just day -> Just (day, c {ccpDay = dz})
|
2023-01-30 21:47:17 -05:00
|
|
|
where
|
2023-02-02 23:18:36 -05:00
|
|
|
y : ys = ccpYear
|
|
|
|
-- TODO not the most efficient way to check weekdays (most likely) since
|
|
|
|
-- I have to go through all the trouble of converting to a day and then
|
|
|
|
-- doing some complex math to figure out which day of the week it is
|
|
|
|
validWeekday day =
|
|
|
|
null ccpWeekly
|
|
|
|
|| (not (null ccpWeekly) && dayToWeekday day `elem` ccpWeekly)
|
|
|
|
dayMaybe m d
|
|
|
|
| d > monthLength y m = Nothing
|
|
|
|
| otherwise =
|
|
|
|
let day = fromGregorian (fromIntegral y) m d
|
|
|
|
in if validWeekday day then Just day else Nothing
|
|
|
|
|
|
|
|
dayToWeekday :: Day -> Int
|
|
|
|
dayToWeekday (ModifiedJulianDay d) = mod (fromIntegral d + 2) 7
|
|
|
|
|
|
|
|
-- -- TODO could clean this up by making an enum instance for Weekday
|
|
|
|
-- dayOfWeek_ :: Int -> Weekday
|
|
|
|
-- dayOfWeek_ d = case d of
|
|
|
|
-- 0 -> Sun
|
|
|
|
-- 1 -> Mon
|
|
|
|
-- 2 -> Tue
|
|
|
|
-- 3 -> Wed
|
|
|
|
-- 4 -> Thu
|
|
|
|
-- 5 -> Fri
|
|
|
|
-- _ -> Sat
|
|
|
|
|
|
|
|
-- weekdayPatternMatches :: WeekdayPat -> Weekday -> Bool
|
|
|
|
-- weekdayPatternMatches (OnDay x) = (== x)
|
|
|
|
-- weekdayPatternMatches (OnDays xs) = (`elem` xs)
|
|
|
|
|
|
|
|
-- 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}) ->
|
|
|
|
-- s >= x && mod x b == 0 && maybe True (\y -> x <= s + b * y) r
|
2022-12-11 17:51:11 -05:00
|
|
|
|
2023-01-30 21:47:17 -05:00
|
|
|
withDates
|
|
|
|
:: MonadUnliftIO m
|
|
|
|
=> DatePat
|
|
|
|
-> (Day -> MappingT m a)
|
|
|
|
-> MappingT m [a]
|
|
|
|
withDates dp f = do
|
|
|
|
bounds <- askBounds
|
2023-02-01 20:56:29 -05:00
|
|
|
let days = expandDatePat bounds dp
|
|
|
|
mapM f days
|
2023-01-30 21:47:17 -05:00
|
|
|
|
|
|
|
askBounds :: MonadUnliftIO m => MappingT m Bounds
|
|
|
|
askBounds = (liftIO . resolveBounds) =<< asks kmBudgetInterval
|
|
|
|
|
2022-12-11 17:51:11 -05:00
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- budget
|
|
|
|
|
2023-01-25 23:04:54 -05:00
|
|
|
insertBudget :: MonadUnliftIO m => Budget -> MappingT m [InsertError]
|
2023-01-30 22:48:16 -05:00
|
|
|
insertBudget Budget {income = is, transfers = 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)
|
2023-01-05 22:16:06 -05:00
|
|
|
whenHash
|
2023-01-24 23:24:41 -05:00
|
|
|
:: (Hashable a, MonadUnliftIO m)
|
2023-01-05 22:16:06 -05:00
|
|
|
=> 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
|
|
|
|
2023-01-30 20:13:25 -05:00
|
|
|
-- TODO allow currency conversions here
|
|
|
|
data BudgetSplit b = BudgetSplit
|
2023-01-30 22:57:42 -05:00
|
|
|
{ bsAcnt :: !AcntID
|
|
|
|
, bsBucket :: !(Maybe b)
|
2023-01-30 20:13:25 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
data BudgetMeta = BudgetMeta
|
2023-01-30 22:57:42 -05:00
|
|
|
{ bmCommit :: !(Key CommitR)
|
|
|
|
, bmWhen :: !Day
|
|
|
|
, bmCur :: !CurID
|
2023-01-30 20:13:25 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
data BudgetTx = BudgetTx
|
2023-01-30 22:57:42 -05:00
|
|
|
{ btMeta :: !BudgetMeta
|
|
|
|
, btFrom :: !(BudgetSplit IncomeBucket)
|
|
|
|
, btTo :: !(BudgetSplit ExpenseBucket)
|
|
|
|
, btValue :: !Rational
|
|
|
|
, btDesc :: !T.Text
|
2023-01-30 20:13:25 -05:00
|
|
|
}
|
|
|
|
|
2023-01-27 20:31:13 -05:00
|
|
|
insertIncome :: MonadUnliftIO m => Income -> MappingT m [InsertError]
|
2023-01-30 20:13:25 -05:00
|
|
|
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
|
|
|
|
2023-01-30 20:13:25 -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
|
|
|
|
}
|
|
|
|
|
|
|
|
balanceIncome :: Income -> EitherErr Rational
|
2023-01-05 22:16:06 -05:00
|
|
|
balanceIncome
|
|
|
|
Income
|
|
|
|
{ incGross = g
|
2023-01-28 19:32:56 -05:00
|
|
|
, incWhen = dp
|
2023-01-05 22:16:06 -05:00
|
|
|
, incPretax = pre
|
|
|
|
, incTaxes = tax
|
|
|
|
, incPosttax = post
|
2023-01-30 20:13:25 -05:00
|
|
|
}
|
2023-01-30 21:12:08 -05:00
|
|
|
| bal < 0 = Left $ IncomeError dp
|
2023-01-30 20:13:25 -05:00
|
|
|
| otherwise = Right bal
|
2023-01-05 22:16:06 -05:00
|
|
|
where
|
2023-01-30 20:13:25 -05:00
|
|
|
bal = dec2Rat g - sum (sumAllocation <$> pre ++ post) - sumTaxes tax
|
2022-12-11 17:51:11 -05:00
|
|
|
|
2023-01-30 20:13:25 -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) ->
|
2023-01-30 20:13:25 -05:00
|
|
|
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}
|
2023-01-30 20:13:25 -05:00
|
|
|
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
|
2023-01-30 20:13:25 -05:00
|
|
|
, btValue = dec2Rat v
|
|
|
|
, btDesc = desc
|
2023-01-05 22:16:06 -05:00
|
|
|
}
|
2022-12-11 17:51:11 -05:00
|
|
|
|
2023-01-30 21:47:17 -05:00
|
|
|
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 b
|
|
|
|
skTo <- insertSplit k sTo
|
|
|
|
bTo <- insert $ BudgetLabelR skTo ""
|
|
|
|
forM_ (bsBucket btTo) $ \b ->
|
|
|
|
insert_ $ ExpenseBucketR bTo b
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
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]
|
2023-01-05 22:16:06 -05:00
|
|
|
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
|
2023-01-05 22:16:06 -05:00
|
|
|
bounds <- (liftIO . resolveBounds) =<< asks kmStatementInterval
|
2023-02-01 20:56:29 -05:00
|
|
|
let days = expandDatePat bounds dp
|
|
|
|
res <- mapM tx days
|
2023-01-28 22:55:07 -05:00
|
|
|
unlessLefts_ (concatEithersL res) $ lift . mapM_ (insertTx c)
|
2023-01-05 22:16:06 -05:00
|
|
|
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
|
|
|
|
|
2023-01-05 22:16:06 -05:00
|
|
|
txPair
|
2023-01-05 22:23:22 -05:00
|
|
|
:: MonadUnliftIO m
|
2023-01-05 22:16:06 -05:00
|
|
|
=> Day
|
|
|
|
-> AcntID
|
|
|
|
-> AcntID
|
2023-01-30 20:13:25 -05:00
|
|
|
-> CurID
|
2023-01-05 22:16:06 -05:00
|
|
|
-> 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
|
2023-01-05 22:16:06 -05:00
|
|
|
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
|
|
|
|
2023-01-28 22:55:07 -05:00
|
|
|
resolveTx :: MonadUnliftIO m => BalTx -> MappingT m (EitherErrs KeyTx)
|
2023-01-05 22:16:06 -05:00
|
|
|
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)
|
2023-01-05 22:16:06 -05:00
|
|
|
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_)
|
|
|
|
}
|
|
|
|
|
2023-01-30 21:47:17 -05:00
|
|
|
insertTx :: MonadUnliftIO m => Key CommitR -> KeyTx -> SqlPersistT m ()
|
|
|
|
insertTx c Tx {txDate = d, txDescr = e, txSplits = ss} = do
|
2023-01-30 20:13:25 -05:00
|
|
|
k <- insert $ TransactionR c d e
|
2022-12-11 17:51:11 -05:00
|
|
|
mapM_ (insertSplit k) ss
|
|
|
|
|
2023-01-30 20:13:25 -05:00
|
|
|
insertSplit :: MonadUnliftIO m => Key TransactionR -> KeySplit -> SqlPersistT m (Key SplitR)
|
2023-01-05 22:16:06 -05:00
|
|
|
insertSplit t Split {sAcnt = aid, sCurrency = cid, sValue = v, sComment = c} = do
|
2023-01-30 20:13:25 -05:00
|
|
|
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
|