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.Statement
|
|
|
|
import Internal.Types hiding (sign)
|
|
|
|
import Internal.Utils
|
|
|
|
import RIO hiding (to)
|
2023-02-12 16:23:32 -05:00
|
|
|
import qualified RIO.List as L
|
|
|
|
import qualified RIO.Map as M
|
2023-02-12 21:52:41 -05:00
|
|
|
import qualified RIO.NonEmpty as NE
|
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-05 10:34:26 -05:00
|
|
|
expandDatePat :: Bounds -> DatePat -> EitherErrs [Day]
|
2023-02-02 23:18:36 -05:00
|
|
|
expandDatePat b (Cron cp) = expandCronPat b cp
|
2023-02-05 10:34:26 -05:00
|
|
|
expandDatePat i (Mod mp) = Right $ expandModPat mp i
|
2022-12-11 17:51:11 -05:00
|
|
|
|
2023-01-28 19:32:56 -05:00
|
|
|
expandModPat :: ModPat -> Bounds -> [Day]
|
2023-02-05 10:34:26 -05:00
|
|
|
expandModPat ModPat {mpStart = s, mpBy = b, mpUnit = u, mpRepeats = r} bs =
|
|
|
|
takeWhile (<= upper) $
|
|
|
|
(`addFun` start) . (* b')
|
|
|
|
<$> maybe id (take . fromIntegral) r [0 ..]
|
2023-02-02 23:18:36 -05:00
|
|
|
where
|
2023-02-05 10:34:26 -05:00
|
|
|
(lower, upper) = expandBounds bs
|
|
|
|
start = maybe lower fromGregorian' s
|
|
|
|
b' = fromIntegral b
|
|
|
|
addFun = case u of
|
|
|
|
Day -> addDays
|
|
|
|
Week -> addDays . (* 7)
|
|
|
|
Month -> addGregorianMonthsClip
|
|
|
|
Year -> addGregorianYearsClip
|
|
|
|
|
|
|
|
expandCronPat :: Bounds -> CronPat -> EitherErrs [Day]
|
2023-02-12 16:23:32 -05:00
|
|
|
expandCronPat b CronPat {cronYear, cronMonth, cronDay, cronWeekly} =
|
|
|
|
concatEither3 yRes mRes dRes $ \ys ms ds ->
|
|
|
|
filter validWeekday $
|
|
|
|
mapMaybe (uncurry3 toDay) $
|
|
|
|
takeWhile (\((y, _), m, d) -> (y, m, d) <= (yb1, mb1, db1)) $
|
|
|
|
dropWhile (\((y, _), m, d) -> (y, m, d) < (yb0, mb0, db0)) $
|
|
|
|
[(y, m, d) | y <- (\y -> (y, isLeapYear y)) <$> ys, m <- ms, d <- ds]
|
2023-01-30 21:47:17 -05:00
|
|
|
where
|
2023-02-05 10:34:26 -05:00
|
|
|
yRes = case cronYear of
|
|
|
|
Nothing -> return [yb0 .. yb1]
|
|
|
|
Just pat -> do
|
2023-02-09 20:01:43 -05:00
|
|
|
ys <- expandMDYPat (fromIntegral yb0) (fromIntegral yb1) pat
|
2023-02-05 10:34:26 -05:00
|
|
|
return $ dropWhile (< yb0) $ fromIntegral <$> ys
|
|
|
|
mRes = expandMD 12 cronMonth
|
|
|
|
dRes = expandMD 31 cronDay
|
|
|
|
(s, e) = expandBounds b
|
|
|
|
(yb0, mb0, db0) = toGregorian s
|
|
|
|
(yb1, mb1, db1) = toGregorian $ addDays (-1) e
|
2023-02-09 20:01:43 -05:00
|
|
|
expandMD lim =
|
|
|
|
fmap (fromIntegral <$>)
|
|
|
|
. maybe (return [1 .. lim]) (expandMDYPat 1 lim)
|
2023-02-05 10:34:26 -05:00
|
|
|
expandW (OnDay x) = [fromEnum x]
|
|
|
|
expandW (OnDays xs) = fromEnum <$> xs
|
|
|
|
ws = maybe [] expandW cronWeekly
|
|
|
|
validWeekday = if null ws then const True else \day -> dayToWeekday day `elem` ws
|
|
|
|
toDay (y, leap) m d
|
|
|
|
| m == 2 && (not leap && d > 28 || leap && d > 29) = Nothing
|
|
|
|
| m `elem` [4, 6, 9, 11] && d > 30 = Nothing
|
|
|
|
| otherwise = Just $ fromGregorian y m d
|
|
|
|
|
2023-02-09 20:01:43 -05:00
|
|
|
expandMDYPat :: Natural -> Natural -> MDYPat -> EitherErr [Natural]
|
|
|
|
expandMDYPat lower upper (Single x) = Right [x | lower <= x && x <= upper]
|
|
|
|
expandMDYPat lower upper (Multi xs) = Right $ dropWhile (<= lower) $ takeWhile (<= upper) xs
|
|
|
|
expandMDYPat lower upper (After x) = Right [max lower x .. upper]
|
|
|
|
expandMDYPat lower upper (Before x) = Right [lower .. min upper x]
|
|
|
|
expandMDYPat lower upper (Between x y) = Right [max lower x .. min upper y]
|
|
|
|
expandMDYPat lower upper (Repeat RepeatPat {rpStart = s, rpBy = b, rpRepeats = r})
|
2023-02-05 10:34:26 -05:00
|
|
|
| b < 1 = Left $ PatternError s b r ZeroLength
|
|
|
|
| otherwise = do
|
|
|
|
k <- limit r
|
2023-02-09 20:01:43 -05:00
|
|
|
return $ dropWhile (<= lower) $ takeWhile (<= k) [s + i * b | i <- [0 ..]]
|
2023-02-05 10:34:26 -05:00
|
|
|
where
|
2023-02-09 20:01:43 -05:00
|
|
|
limit Nothing = Right upper
|
2023-02-05 10:34:26 -05:00
|
|
|
limit (Just n)
|
|
|
|
-- this guard not only produces the error for the user but also protects
|
|
|
|
-- from an underflow below it
|
|
|
|
| n < 1 = Left $ PatternError s b r ZeroRepeats
|
2023-02-09 20:01:43 -05:00
|
|
|
| otherwise = Right $ min (s + b * (n - 1)) upper
|
2023-02-02 23:18:36 -05:00
|
|
|
|
|
|
|
dayToWeekday :: Day -> Int
|
|
|
|
dayToWeekday (ModifiedJulianDay d) = mod (fromIntegral d + 2) 7
|
|
|
|
|
2023-01-30 21:47:17 -05:00
|
|
|
withDates
|
2023-02-12 16:23:32 -05:00
|
|
|
:: MonadFinance m
|
2023-01-30 21:47:17 -05:00
|
|
|
=> DatePat
|
2023-02-12 21:52:41 -05:00
|
|
|
-> (Day -> SqlPersistT m (EitherErrs a))
|
2023-02-12 16:23:32 -05:00
|
|
|
-> SqlPersistT m (EitherErrs [a])
|
2023-01-30 21:47:17 -05:00
|
|
|
withDates dp f = do
|
2023-02-12 16:23:32 -05:00
|
|
|
bounds <- lift $ askDBState kmBudgetInterval
|
2023-02-12 21:52:41 -05:00
|
|
|
case expandDatePat bounds dp of
|
|
|
|
Left es -> return $ Left es
|
|
|
|
Right days -> concatEithersL <$> mapM f days
|
2023-01-30 21:47:17 -05:00
|
|
|
|
2022-12-11 17:51:11 -05:00
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- budget
|
|
|
|
|
2023-02-12 17:00:29 -05:00
|
|
|
-- 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 (TODO)
|
|
|
|
-- 5. insert all transactions
|
|
|
|
|
2023-02-12 16:23:32 -05:00
|
|
|
insertBudget :: MonadFinance m => Budget -> SqlPersistT m [InsertError]
|
2023-03-16 23:53:57 -04:00
|
|
|
insertBudget
|
|
|
|
b@( Budget
|
|
|
|
{ budgetLabel
|
|
|
|
, incomes
|
|
|
|
, transfers
|
|
|
|
, shadowTransfers
|
|
|
|
, pretax
|
|
|
|
, tax
|
|
|
|
, posttax
|
|
|
|
}
|
|
|
|
) =
|
|
|
|
whenHash CTBudget b [] $ \key -> do
|
|
|
|
unlessLefts intAllos $ \intAllos_ -> do
|
|
|
|
res1 <- mapM (insertIncome key budgetLabel intAllos_) incomes
|
|
|
|
res2 <- expandTransfers key budgetLabel transfers
|
|
|
|
unlessLefts (concatEithers2 (fmap concat $ concatEithersL res1) res2 (++)) $
|
|
|
|
\txs -> do
|
|
|
|
unlessLefts (addShadowTransfers shadowTransfers txs) $ \shadow -> do
|
|
|
|
let bals = balanceTransfers $ txs ++ shadow
|
|
|
|
concat <$> mapM insertBudgetTx bals
|
|
|
|
where
|
|
|
|
intAllos =
|
|
|
|
let pre_ = sortAllos pretax
|
|
|
|
tax_ = sortAllos tax
|
|
|
|
post_ = sortAllos posttax
|
|
|
|
in concatEithers3 pre_ tax_ post_ (,,)
|
|
|
|
sortAllos = concatEithersL . fmap sortAllo
|
|
|
|
|
|
|
|
type BoundAllocation = Allocation_ (TimeAmount (Day, Day))
|
|
|
|
|
|
|
|
type IntAllocations = ([BoundAllocation], [BoundAllocation], [BoundAllocation])
|
|
|
|
|
|
|
|
-- TODO this should actually error if there is no ultimate end date
|
|
|
|
sortAllo :: IntervalAllocation -> EitherErrs BoundAllocation
|
|
|
|
sortAllo a@Allocation_ {alloAmts = as} = do
|
2023-04-12 22:58:31 -04:00
|
|
|
bs <- fmap reverse <$> foldBounds (Right []) $ L.sortOn taWhen as
|
2023-03-16 23:53:57 -04:00
|
|
|
return $ a {alloAmts = L.sort bs}
|
|
|
|
where
|
|
|
|
foldBounds acc [] = acc
|
|
|
|
foldBounds acc (x : xs) =
|
|
|
|
let res = fmap (fmap expandBounds) $ case xs of
|
|
|
|
[] -> mapM resolveBounds x
|
|
|
|
(y : _) ->
|
|
|
|
let end = intStart $ taWhen y
|
|
|
|
in mapM (resolveBounds_ end) x
|
|
|
|
in foldBounds (concatEithers2 (plural res) acc (:)) xs
|
2023-02-13 19:57:39 -05:00
|
|
|
|
|
|
|
-- TODO this is going to be O(n*m), which might be a problem?
|
|
|
|
addShadowTransfers :: [ShadowTransfer] -> [BudgetTxType] -> EitherErrs [BudgetTxType]
|
|
|
|
addShadowTransfers ms txs =
|
|
|
|
fmap catMaybes $
|
|
|
|
concatEitherL $
|
|
|
|
fmap (uncurry fromShadow) $
|
|
|
|
[(t, m) | t <- txs, m <- ms]
|
|
|
|
|
|
|
|
fromShadow :: BudgetTxType -> ShadowTransfer -> EitherErr (Maybe BudgetTxType)
|
|
|
|
fromShadow tx t@ShadowTransfer {stFrom, stTo, stDesc, stRatio} = do
|
|
|
|
res <- shadowMatches (stMatch t) tx
|
|
|
|
return $
|
|
|
|
if not res
|
|
|
|
then Nothing
|
|
|
|
else
|
|
|
|
Just $
|
|
|
|
BudgetTxType
|
|
|
|
{ bttTx =
|
|
|
|
-- TODO does this actually share the same metadata as the "parent" tx?
|
|
|
|
BudgetTx
|
|
|
|
{ btMeta = btMeta $ bttTx tx
|
|
|
|
, btWhen = btWhen $ bttTx tx
|
2023-02-26 18:57:40 -05:00
|
|
|
, btFrom = stFrom
|
|
|
|
, btTo = stTo
|
2023-02-13 19:57:39 -05:00
|
|
|
, btValue = dec2Rat stRatio * (btValue $ bttTx tx)
|
|
|
|
, btDesc = stDesc
|
|
|
|
}
|
|
|
|
, bttType = FixedAmt
|
|
|
|
}
|
|
|
|
|
|
|
|
shadowMatches :: ShadowMatch -> BudgetTxType -> EitherErr Bool
|
|
|
|
shadowMatches ShadowMatch {smFrom, smTo, smDate, smVal} tx = do
|
|
|
|
-- TODO what does the amount do for each of the different types?
|
|
|
|
valRes <- valMatches smVal (btValue tx_)
|
|
|
|
return $
|
2023-02-26 22:53:12 -05:00
|
|
|
memberMaybe (taAcnt $ btFrom tx_) smFrom
|
|
|
|
&& memberMaybe (taAcnt $ btTo tx_) smTo
|
2023-02-13 19:57:39 -05:00
|
|
|
&& maybe True (`dateMatches` (btWhen tx_)) smDate
|
|
|
|
&& valRes
|
|
|
|
where
|
|
|
|
tx_ = bttTx tx
|
2023-02-26 11:27:11 -05:00
|
|
|
memberMaybe x AcntSet {asList, asInclude} =
|
|
|
|
(if asInclude then id else not) $ x `elem` asList
|
2022-12-11 17:51:11 -05:00
|
|
|
|
2023-02-12 22:18:31 -05:00
|
|
|
balanceTransfers :: [BudgetTxType] -> [BudgetTx]
|
2023-03-01 20:38:11 -05:00
|
|
|
balanceTransfers ts =
|
|
|
|
snd $ L.mapAccumR go initBals $ reverse $ L.sortOn (btWhen . bttTx) ts
|
2023-02-12 22:18:31 -05:00
|
|
|
where
|
|
|
|
initBals =
|
|
|
|
M.fromList $
|
|
|
|
fmap (,0) $
|
|
|
|
L.nub $
|
2023-03-01 20:38:11 -05:00
|
|
|
fmap (btTo . bttTx) ts
|
|
|
|
++ fmap (btFrom . bttTx) ts
|
2023-02-12 22:18:31 -05:00
|
|
|
updateBal x = M.update (Just . (+ x))
|
|
|
|
lookupBal = M.findWithDefault (error "this should not happen")
|
|
|
|
go bals btt =
|
|
|
|
let tx = bttTx btt
|
2023-02-26 18:57:40 -05:00
|
|
|
from = btFrom tx
|
|
|
|
to = btTo tx
|
2023-02-12 22:18:31 -05:00
|
|
|
bal = lookupBal to bals
|
|
|
|
x = amtToMove bal (bttType btt) (btValue tx)
|
|
|
|
in (updateBal x to $ updateBal (-x) from bals, tx {btValue = x})
|
|
|
|
-- 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
|
|
|
|
-- target value
|
|
|
|
amtToMove _ FixedAmt x = x
|
|
|
|
amtToMove bal Percent x = -(x / 100 * bal)
|
|
|
|
amtToMove bal Target x = x - bal
|
|
|
|
|
2023-01-30 20:13:25 -05:00
|
|
|
data BudgetMeta = BudgetMeta
|
2023-03-01 20:38:11 -05:00
|
|
|
{ bmCommit :: !CommitRId
|
2023-02-26 12:03:35 -05:00
|
|
|
, bmCur :: !BudgetCurrency
|
2023-02-05 18:45:56 -05:00
|
|
|
, bmName :: !T.Text
|
2023-01-30 20:13:25 -05:00
|
|
|
}
|
2023-04-12 22:58:31 -04:00
|
|
|
deriving (Show)
|
2023-01-30 20:13:25 -05:00
|
|
|
|
|
|
|
data BudgetTx = BudgetTx
|
2023-01-30 22:57:42 -05:00
|
|
|
{ btMeta :: !BudgetMeta
|
2023-02-12 22:18:31 -05:00
|
|
|
, btWhen :: !Day
|
2023-02-26 22:53:12 -05:00
|
|
|
, btFrom :: !TaggedAcnt
|
|
|
|
, btTo :: !TaggedAcnt
|
2023-01-30 22:57:42 -05:00
|
|
|
, btValue :: !Rational
|
2023-02-05 18:45:56 -05:00
|
|
|
, btDesc :: !T.Text
|
2023-01-30 20:13:25 -05:00
|
|
|
}
|
2023-04-12 22:58:31 -04:00
|
|
|
deriving (Show)
|
2023-01-30 20:13:25 -05:00
|
|
|
|
2023-02-12 22:18:31 -05:00
|
|
|
data BudgetTxType = BudgetTxType
|
|
|
|
{ bttType :: !AmountType
|
|
|
|
, bttTx :: !BudgetTx
|
2023-02-12 16:52:42 -05:00
|
|
|
}
|
2023-04-12 22:58:31 -04:00
|
|
|
deriving (Show)
|
2023-02-12 16:52:42 -05:00
|
|
|
|
2023-03-16 23:53:57 -04:00
|
|
|
insertIncome
|
|
|
|
:: MonadFinance m
|
|
|
|
=> CommitRId
|
|
|
|
-> T.Text
|
|
|
|
-> IntAllocations
|
|
|
|
-> Income
|
|
|
|
-> SqlPersistT m (EitherErrs [BudgetTxType])
|
2023-02-12 16:23:32 -05:00
|
|
|
insertIncome
|
2023-03-01 20:38:11 -05:00
|
|
|
key
|
2023-02-12 16:23:32 -05:00
|
|
|
name
|
2023-03-16 23:53:57 -04:00
|
|
|
(intPre, intTax, intPost)
|
2023-03-01 20:38:11 -05:00
|
|
|
i@Income {incWhen, incCurrency, incFrom, incPretax, incPosttax, incTaxes, incToBal} = do
|
|
|
|
let meta = BudgetMeta key (NoX incCurrency) name
|
|
|
|
let balRes = balanceIncome i
|
|
|
|
fromRes <- lift $ checkAcntType IncomeT $ taAcnt incFrom
|
|
|
|
case concatEither2 balRes fromRes (,) of
|
|
|
|
Left es -> return $ Left es
|
|
|
|
-- TODO this hole seems sloppy...
|
|
|
|
Right (balance, _) ->
|
|
|
|
fmap (fmap (concat . concat)) $
|
2023-03-16 23:53:57 -04:00
|
|
|
-- TODO this will scan the interval allocations fully each time
|
|
|
|
-- iteration which is a total waste, but the fix requires turning this
|
|
|
|
-- loop into a fold which I don't feel like doing now :(
|
2023-03-01 20:38:11 -05:00
|
|
|
withDates incWhen $ \day -> do
|
|
|
|
let fromAllos = fmap concat . mapM (lift . fromAllo day meta incFrom)
|
2023-03-16 23:53:57 -04:00
|
|
|
pre <- fromAllos $ incPretax ++ mapMaybe (selectAllos day) intPre
|
|
|
|
-- TODO ensure these are all expense accounts
|
|
|
|
tax <- fromAllos $ incTaxes ++ mapMaybe (selectAllos day) intTax
|
|
|
|
post <- fromAllos $ incPosttax ++ mapMaybe (selectAllos day) intPost
|
2023-03-01 20:38:11 -05:00
|
|
|
let bal =
|
|
|
|
BudgetTxType
|
|
|
|
{ bttTx =
|
|
|
|
BudgetTx
|
|
|
|
{ btMeta = meta
|
|
|
|
, btWhen = day
|
|
|
|
, btFrom = incFrom
|
|
|
|
, btTo = incToBal
|
|
|
|
, btValue = balance
|
|
|
|
, btDesc = "balance after deductions"
|
|
|
|
}
|
|
|
|
, bttType = FixedAmt
|
|
|
|
}
|
2023-03-16 23:53:57 -04:00
|
|
|
return $ concatEithersL [Right [bal], Right tax, Right pre, Right post]
|
|
|
|
|
|
|
|
-- ASSUME allocations are sorted
|
|
|
|
selectAllos :: Day -> BoundAllocation -> Maybe Allocation
|
|
|
|
selectAllos day a@Allocation_ {alloAmts = as} = case select [] as of
|
|
|
|
[] -> Nothing
|
|
|
|
xs -> Just $ a {alloAmts = xs}
|
|
|
|
where
|
|
|
|
select acc [] = acc
|
|
|
|
select acc (x : xs)
|
2023-04-12 22:58:31 -04:00
|
|
|
| day < fst (taWhen x) = select acc xs
|
2023-03-16 23:53:57 -04:00
|
|
|
| inBounds (taWhen x) day = select (taAmt x : acc) xs
|
|
|
|
| otherwise = acc
|
2023-01-30 20:13:25 -05:00
|
|
|
|
|
|
|
fromAllo
|
2023-02-12 21:52:41 -05:00
|
|
|
:: MonadFinance m
|
2023-02-12 22:18:31 -05:00
|
|
|
=> Day
|
|
|
|
-> BudgetMeta
|
2023-02-26 22:53:12 -05:00
|
|
|
-> TaggedAcnt
|
2023-01-30 20:13:25 -05:00
|
|
|
-> Allocation
|
2023-02-26 18:57:40 -05:00
|
|
|
-> m [BudgetTxType]
|
2023-03-16 23:53:57 -04:00
|
|
|
fromAllo day meta from Allocation_ {alloTo, alloAmts} = do
|
2023-02-12 21:52:41 -05:00
|
|
|
-- TODO this is going to be repeated a zillion times (might matter)
|
2023-02-26 18:57:40 -05:00
|
|
|
-- res <- expandTarget alloPath
|
|
|
|
return $ fmap toBT alloAmts
|
2023-01-30 20:13:25 -05:00
|
|
|
where
|
2023-04-17 00:34:09 -04:00
|
|
|
toBT (Amount {amtDesc = desc, amtValue = v}) =
|
2023-02-12 22:18:31 -05:00
|
|
|
BudgetTxType
|
|
|
|
{ bttTx =
|
|
|
|
BudgetTx
|
|
|
|
{ btFrom = from
|
|
|
|
, btWhen = day
|
2023-02-26 22:53:12 -05:00
|
|
|
, btTo = alloTo
|
2023-02-12 22:18:31 -05:00
|
|
|
, btValue = dec2Rat v
|
|
|
|
, btDesc = desc
|
|
|
|
, btMeta = meta
|
|
|
|
}
|
|
|
|
, bttType = FixedAmt
|
2023-01-30 20:13:25 -05:00
|
|
|
}
|
2023-01-27 20:31:13 -05:00
|
|
|
|
2023-03-16 23:53:57 -04:00
|
|
|
-- -- TODO maybe allow tags here?
|
|
|
|
-- fromTax
|
|
|
|
-- :: MonadFinance m
|
|
|
|
-- => Day
|
|
|
|
-- -> BudgetMeta
|
|
|
|
-- -> AcntID
|
|
|
|
-- -> Tax
|
|
|
|
-- -> m (EitherErr BudgetTxType)
|
|
|
|
-- fromTax day meta from Tax {taxAcnt = to, taxValue = v} = do
|
|
|
|
-- res <- checkAcntType ExpenseT to
|
|
|
|
-- return $ fmap go res
|
|
|
|
-- where
|
|
|
|
-- go to_ =
|
|
|
|
-- BudgetTxType
|
|
|
|
-- { bttTx =
|
|
|
|
-- BudgetTx
|
|
|
|
-- { btFrom = TaggedAcnt from []
|
|
|
|
-- , btWhen = day
|
|
|
|
-- , btTo = TaggedAcnt to_ []
|
|
|
|
-- , btValue = dec2Rat v
|
|
|
|
-- , btDesc = ""
|
|
|
|
-- , btMeta = meta
|
|
|
|
-- }
|
|
|
|
-- , bttType = FixedAmt
|
|
|
|
-- }
|
2023-01-30 20:13:25 -05:00
|
|
|
|
|
|
|
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-03-16 23:53:57 -04:00
|
|
|
bal = dec2Rat g - sum (sumAllocation <$> pre ++ tax ++ post)
|
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
|
|
|
|
2023-03-16 23:53:57 -04:00
|
|
|
-- sumTaxes :: [Tax] -> Rational
|
|
|
|
-- sumTaxes = sum . fmap (dec2Rat . taxValue)
|
2022-12-11 17:51:11 -05:00
|
|
|
|
2023-02-12 22:18:31 -05:00
|
|
|
expandTransfers
|
|
|
|
:: MonadFinance m
|
2023-03-01 20:38:11 -05:00
|
|
|
=> CommitRId
|
|
|
|
-> T.Text
|
2023-02-12 22:18:31 -05:00
|
|
|
-> [Transfer]
|
|
|
|
-> SqlPersistT m (EitherErrs [BudgetTxType])
|
2023-03-01 20:38:11 -05:00
|
|
|
expandTransfers key name ts = do
|
|
|
|
txs <- mapM (expandTransfer key name) ts
|
2023-02-12 22:18:31 -05:00
|
|
|
return $ L.sortOn (btWhen . bttTx) . concat <$> concatEithersL txs
|
2023-02-12 16:23:32 -05:00
|
|
|
|
2023-03-01 20:38:11 -05:00
|
|
|
expandTransfer :: MonadFinance m => CommitRId -> T.Text -> Transfer -> SqlPersistT m (EitherErrs [BudgetTxType])
|
|
|
|
expandTransfer key name Transfer {transAmounts, transTo, transCurrency, transFrom} =
|
|
|
|
-- whenHash CTExpense t (Right []) $ \key ->
|
|
|
|
fmap (fmap concat . concatEithersL) $
|
2023-04-17 00:34:09 -04:00
|
|
|
forM transAmounts $ \(TimeAmount {taWhen = pat, taAmt = (Amount {amtDesc = desc, amtValue = v}), taAmtType = atype}) -> do
|
2023-03-01 20:38:11 -05:00
|
|
|
withDates pat $ \day ->
|
|
|
|
let meta =
|
|
|
|
BudgetMeta
|
|
|
|
{ bmCur = transCurrency
|
|
|
|
, bmCommit = key
|
|
|
|
, bmName = name
|
|
|
|
}
|
|
|
|
tx =
|
|
|
|
BudgetTxType
|
|
|
|
{ bttTx =
|
|
|
|
BudgetTx
|
|
|
|
{ btMeta = meta
|
|
|
|
, btWhen = day
|
|
|
|
, btFrom = transFrom
|
|
|
|
, btTo = transTo
|
|
|
|
, btValue = dec2Rat v
|
|
|
|
, btDesc = desc
|
|
|
|
}
|
|
|
|
, bttType = atype
|
|
|
|
}
|
|
|
|
in return $ Right tx
|
2023-02-12 16:23:32 -05:00
|
|
|
|
|
|
|
insertBudgetTx :: MonadFinance m => BudgetTx -> SqlPersistT m [InsertError]
|
2023-02-12 22:18:31 -05:00
|
|
|
insertBudgetTx BudgetTx {btFrom, btTo, btMeta, btValue, btDesc, btWhen} = do
|
2023-02-26 18:57:40 -05:00
|
|
|
res <- lift $ splitPair btFrom btTo (bmCur btMeta) btValue
|
2023-02-26 12:03:35 -05:00
|
|
|
unlessLefts_ res $ \((sFrom, sTo), exchange) -> do
|
|
|
|
insertPair sFrom sTo
|
|
|
|
forM_ exchange $ \(xFrom, xTo) -> insertPair xFrom xTo
|
2023-02-05 18:45:56 -05:00
|
|
|
where
|
2023-02-26 12:03:35 -05:00
|
|
|
insertPair from to = do
|
|
|
|
k <- insert $ TransactionR (bmCommit btMeta) btWhen btDesc
|
2023-02-26 18:57:40 -05:00
|
|
|
insertBudgetLabel k from
|
|
|
|
insertBudgetLabel k to
|
|
|
|
insertBudgetLabel k split = do
|
|
|
|
sk <- insertSplit k split
|
|
|
|
insert_ $ BudgetLabelR sk $ bmName btMeta
|
2023-01-30 21:47:17 -05:00
|
|
|
|
2023-02-26 12:03:35 -05:00
|
|
|
type SplitPair = (KeySplit, KeySplit)
|
|
|
|
|
2023-01-30 21:47:17 -05:00
|
|
|
splitPair
|
2023-02-12 16:23:32 -05:00
|
|
|
:: MonadFinance m
|
2023-02-26 22:53:12 -05:00
|
|
|
=> TaggedAcnt
|
|
|
|
-> TaggedAcnt
|
2023-02-26 12:03:35 -05:00
|
|
|
-> BudgetCurrency
|
2023-01-30 21:47:17 -05:00
|
|
|
-> Rational
|
2023-02-26 12:03:35 -05:00
|
|
|
-> m (EitherErrs (SplitPair, Maybe SplitPair))
|
|
|
|
splitPair from to cur val = case cur of
|
|
|
|
NoX curid -> fmap (fmap (,Nothing)) $ pair curid from to val
|
|
|
|
X (Exchange {xFromCur, xToCur, xAcnt, xRate}) -> do
|
2023-02-26 22:53:12 -05:00
|
|
|
let middle = TaggedAcnt xAcnt []
|
|
|
|
res1 <- pair xFromCur from middle val
|
|
|
|
res2 <- pair xToCur middle to (val * dec2Rat xRate)
|
2023-02-26 12:03:35 -05:00
|
|
|
return $ concatEithers2 res1 res2 $ \a b -> (a, Just b)
|
2023-01-30 21:47:17 -05:00
|
|
|
where
|
2023-02-26 12:03:35 -05:00
|
|
|
pair curid from_ to_ v = do
|
|
|
|
s1 <- split curid from_ (-v)
|
|
|
|
s2 <- split curid to_ v
|
|
|
|
return $ concatEithers2 s1 s2 (,)
|
2023-02-26 22:53:12 -05:00
|
|
|
split c TaggedAcnt {taAcnt, taTags} v =
|
2023-01-30 21:47:17 -05:00
|
|
|
resolveSplit $
|
|
|
|
Split
|
2023-02-26 22:53:12 -05:00
|
|
|
{ sAcnt = taAcnt
|
2023-01-30 21:47:17 -05:00
|
|
|
, sValue = v
|
|
|
|
, sComment = ""
|
2023-02-26 12:03:35 -05:00
|
|
|
, sCurrency = c
|
2023-02-26 22:53:12 -05:00
|
|
|
, sTags = taTags
|
2023-01-30 21:47:17 -05:00
|
|
|
}
|
|
|
|
|
2023-02-12 22:18:31 -05:00
|
|
|
checkAcntType
|
|
|
|
:: MonadFinance m
|
|
|
|
=> AcntType
|
|
|
|
-> AcntID
|
2023-02-26 18:57:40 -05:00
|
|
|
-> m (EitherErr AcntID)
|
2023-02-12 22:18:31 -05:00
|
|
|
checkAcntType t = checkAcntTypes (t :| [])
|
|
|
|
|
|
|
|
checkAcntTypes
|
|
|
|
:: MonadFinance m
|
|
|
|
=> NE.NonEmpty AcntType
|
|
|
|
-> AcntID
|
2023-02-26 18:57:40 -05:00
|
|
|
-> m (EitherErr AcntID)
|
|
|
|
checkAcntTypes ts i = (go =<<) <$> lookupAccountType i
|
2023-02-12 22:18:31 -05:00
|
|
|
where
|
|
|
|
go t
|
2023-02-26 18:57:40 -05:00
|
|
|
| t `L.elem` ts = Right i
|
2023-02-25 22:56:23 -05:00
|
|
|
| otherwise = Left $ AccountError i ts
|
2023-02-12 22:18:31 -05:00
|
|
|
|
2022-12-11 17:51:11 -05:00
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- statements
|
|
|
|
|
2023-02-12 16:23:32 -05:00
|
|
|
insertStatements :: MonadFinance m => Config -> SqlPersistT 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-02-12 16:23:32 -05:00
|
|
|
insertStatement :: MonadFinance m => Statement -> SqlPersistT 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-02-12 16:23:32 -05:00
|
|
|
insertManual :: MonadFinance m => Manual -> SqlPersistT 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-02-12 16:23:32 -05:00
|
|
|
bounds <- lift $ askDBState kmStatementInterval
|
2023-02-05 10:34:26 -05:00
|
|
|
-- let days = expandDatePat bounds dp
|
|
|
|
let dayRes = expandDatePat bounds dp
|
|
|
|
unlessLefts dayRes $ \days -> do
|
2023-02-12 21:52:41 -05:00
|
|
|
txRes <- mapM (lift . tx) days
|
2023-02-12 16:23:32 -05:00
|
|
|
unlessLefts_ (concatEithersL txRes) $ 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-02-12 16:23:32 -05:00
|
|
|
insertImport :: MonadFinance m => Import -> SqlPersistT m [InsertError]
|
2023-01-27 20:31:13 -05:00
|
|
|
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-02-12 16:23:32 -05:00
|
|
|
recoverIO (lift $ readImport i) $ \r -> unlessLefts r $ \bs -> do
|
|
|
|
bounds <- expandBounds <$> lift (askDBState kmStatementInterval)
|
2023-02-12 21:52:41 -05:00
|
|
|
res <- mapM (lift . resolveTx) $ filter (inBounds bounds . txDate) bs
|
2023-02-12 16:23:32 -05:00
|
|
|
unlessLefts_ (concatEithersL res) $ mapM_ (insertTx c)
|
2023-01-28 22:55:07 -05:00
|
|
|
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-02-26 22:53:12 -05:00
|
|
|
-- TODO tags here?
|
2023-01-05 22:16:06 -05:00
|
|
|
txPair
|
2023-02-12 16:23:32 -05:00
|
|
|
:: MonadFinance 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-02-12 21:52:41 -05:00
|
|
|
-> m (EitherErrs KeyTx)
|
2022-12-11 17:51:11 -05:00
|
|
|
txPair day from to cur val desc = resolveTx tx
|
|
|
|
where
|
2023-02-26 22:53:12 -05:00
|
|
|
split a v = Split {sAcnt = a, sValue = v, sComment = "", sCurrency = cur, sTags = []}
|
2023-01-05 22:16:06 -05:00
|
|
|
tx =
|
|
|
|
Tx
|
|
|
|
{ txDescr = desc
|
|
|
|
, txDate = day
|
|
|
|
, txSplits = [split from (-val), split to val]
|
|
|
|
}
|
2022-12-11 17:51:11 -05:00
|
|
|
|
2023-02-12 21:52:41 -05:00
|
|
|
resolveTx :: MonadFinance m => BalTx -> 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-02-12 21:52:41 -05:00
|
|
|
resolveSplit :: MonadFinance m => BalSplit -> m (EitherErrs KeySplit)
|
2023-02-26 22:53:12 -05:00
|
|
|
resolveSplit s@Split {sAcnt, sCurrency, sValue, sTags} = do
|
|
|
|
aid <- lookupAccountKey sAcnt
|
|
|
|
cid <- lookupCurrency sCurrency
|
|
|
|
sign <- lookupAccountSign sAcnt
|
|
|
|
tags <- mapM lookupTag sTags
|
2022-12-11 17:51:11 -05:00
|
|
|
-- TODO correct sign here?
|
|
|
|
-- TODO lenses would be nice here
|
2023-02-26 22:53:12 -05:00
|
|
|
return $
|
|
|
|
(concatEithers2 (concatEither3 aid cid sign (,,)) $ concatEitherL tags) $
|
|
|
|
\(aid_, cid_, sign_) tags_ ->
|
|
|
|
s
|
|
|
|
{ sAcnt = aid_
|
|
|
|
, sCurrency = cid_
|
|
|
|
, sValue = sValue * fromIntegral (sign2Int sign_)
|
|
|
|
, sTags = tags_
|
|
|
|
}
|
2023-01-28 22:55:07 -05:00
|
|
|
|
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-02-26 22:53:12 -05:00
|
|
|
insertSplit t Split {sAcnt, sCurrency, sValue, sComment, sTags} = do
|
|
|
|
k <- insert $ SplitR t sCurrency sAcnt sComment sValue
|
|
|
|
mapM_ (insert_ . TagRelationR k) sTags
|
|
|
|
return k
|
2023-01-28 22:55:07 -05:00
|
|
|
|
2023-02-12 21:52:41 -05:00
|
|
|
lookupAccount :: MonadFinance m => AcntID -> m (EitherErr (Key AccountR, AcntSign, AcntType))
|
|
|
|
lookupAccount p = lookupErr (DBKey AcntField) p <$> (askDBState kmAccount)
|
|
|
|
|
|
|
|
lookupAccountKey :: MonadFinance m => AcntID -> m (EitherErr (Key AccountR))
|
|
|
|
lookupAccountKey = (fmap (fmap fstOf3)) . lookupAccount
|
2023-01-28 22:55:07 -05:00
|
|
|
|
2023-02-12 21:52:41 -05:00
|
|
|
lookupAccountSign :: MonadFinance m => AcntID -> m (EitherErr AcntSign)
|
|
|
|
lookupAccountSign = fmap (fmap sndOf3) . lookupAccount
|
2023-01-28 22:55:07 -05:00
|
|
|
|
2023-02-12 21:52:41 -05:00
|
|
|
lookupAccountType :: MonadFinance m => AcntID -> m (EitherErr AcntType)
|
|
|
|
lookupAccountType = fmap (fmap thdOf3) . lookupAccount
|
2023-01-28 22:55:07 -05:00
|
|
|
|
2023-02-12 21:52:41 -05:00
|
|
|
lookupCurrency :: MonadFinance m => T.Text -> m (EitherErr (Key CurrencyR))
|
|
|
|
lookupCurrency c = lookupErr (DBKey CurField) c <$> (askDBState kmCurrency)
|
2023-02-12 16:52:42 -05:00
|
|
|
|
2023-02-26 22:53:12 -05:00
|
|
|
lookupTag :: MonadFinance m => TagID -> m (EitherErr (Key TagR))
|
|
|
|
lookupTag c = lookupErr (DBKey TagField) c <$> (askDBState kmTag)
|
|
|
|
|
2023-02-12 16:52:42 -05:00
|
|
|
-- TODO this hashes twice (not that it really matters)
|
|
|
|
whenHash
|
|
|
|
:: (Hashable a, MonadFinance m)
|
|
|
|
=> ConfigType
|
|
|
|
-> a
|
|
|
|
-> b
|
|
|
|
-> (Key CommitR -> SqlPersistT m b)
|
|
|
|
-> SqlPersistT m b
|
|
|
|
whenHash t o def f = do
|
|
|
|
let h = hash o
|
|
|
|
hs <- lift $ askDBState kmNewCommits
|
|
|
|
if h `elem` hs then f =<< insert (CommitR h t) else return def
|