pwncash/lib/Internal/Insert.hs

565 lines
19 KiB
Haskell
Raw Normal View History

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)
2023-02-12 21:52:41 -05:00
import GHC.Utils.Misc hiding (split)
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
import qualified RIO.Text as T
import RIO.Time
2022-12-11 17:51:11 -05:00
--------------------------------------------------------------------------------
-- intervals
expandDatePat :: Bounds -> DatePat -> EitherErrs [Day]
2023-02-02 23:18:36 -05:00
expandDatePat b (Cron cp) = expandCronPat b cp
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]
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
(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
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
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)
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})
| 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 ..]]
where
2023-02-09 20:01:43 -05:00
limit Nothing = Right upper
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]
insertBudget b@(Budget {budgetLabel = name, income = is, transfers = es, shadowTransfers = ss}) =
whenHash CTBudget b [] $ \key -> do
res1 <- mapM (insertIncome key name) is
res2 <- expandTransfers key name es
unlessLefts (concatEithers2 (fmap concat $ concatEithersL res1) res2 (++)) $
\txs -> do
unlessLefts (addShadowTransfers ss txs) $ \shadow -> do
let bals = balanceTransfers $ txs ++ shadow
concat <$> mapM insertBudgetTx bals
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
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]
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 $
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
data BudgetMeta = BudgetMeta
{ bmCommit :: !CommitRId
2023-02-26 12:03:35 -05:00
, bmCur :: !BudgetCurrency
2023-02-05 18:45:56 -05:00
, bmName :: !T.Text
}
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-02-12 22:18:31 -05:00
data BudgetTxType = BudgetTxType
{ bttType :: !AmountType
, bttTx :: !BudgetTx
2023-02-12 16:52:42 -05:00
}
insertIncome :: MonadFinance m => CommitRId -> T.Text -> Income -> SqlPersistT m (EitherErrs [BudgetTxType])
2023-02-12 16:23:32 -05:00
insertIncome
key
2023-02-12 16:23:32 -05:00
name
i@Income {incWhen, incCurrency, incFrom, incPretax, incPosttax, incTaxes, incToBal} = do
-- whenHash CTIncome i (Right []) $ \c -> 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)) $
withDates incWhen $ \day -> do
let fromAllos = fmap concat . mapM (lift . fromAllo day meta incFrom)
pre <- fromAllos incPretax
tax <-
concatEitherL
<$> mapM (lift . fromTax day meta (taAcnt incFrom)) incTaxes
post <- fromAllos incPosttax
let bal =
BudgetTxType
{ bttTx =
BudgetTx
{ btMeta = meta
, btWhen = day
, btFrom = incFrom
, btTo = incToBal
, btValue = balance
, btDesc = "balance after deductions"
}
, bttType = FixedAmt
}
return $ concatEithersL [Right [bal], tax, Right pre, Right post]
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
-> Allocation
2023-02-26 18:57:40 -05:00
-> m [BudgetTxType]
2023-02-26 22:53:12 -05: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
where
2023-02-26 18:57:40 -05:00
toBT (Amount desc 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-27 20:31:13 -05:00
2023-02-26 22:53:12 -05:00
-- TODO maybe allow tags here?
2023-02-12 21:52:41 -05:00
fromTax
:: MonadFinance m
2023-02-12 22:18:31 -05:00
=> Day
-> BudgetMeta
2023-02-26 18:57:40 -05:00
-> AcntID
2023-02-12 21:52:41 -05:00
-> Tax
2023-02-12 22:18:31 -05:00
-> m (EitherErr BudgetTxType)
2023-02-26 18:57:40 -05:00
fromTax day meta from Tax {taxAcnt = to, taxValue = v} = do
res <- checkAcntType ExpenseT to
return $ fmap go res
where
go to_ =
BudgetTxType
{ bttTx =
BudgetTx
2023-02-26 22:53:12 -05:00
{ btFrom = TaggedAcnt from []
2023-02-26 18:57:40 -05:00
, btWhen = day
2023-02-26 22:53:12 -05:00
, btTo = TaggedAcnt to_ []
2023-02-26 18:57:40 -05:00
, btValue = dec2Rat v
, btDesc = ""
, btMeta = meta
}
, bttType = FixedAmt
}
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-02-12 22:18:31 -05:00
expandTransfers
:: MonadFinance m
=> CommitRId
-> T.Text
2023-02-12 22:18:31 -05:00
-> [Transfer]
-> SqlPersistT m (EitherErrs [BudgetTxType])
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
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) $
forM transAmounts $ \(TimeAmount (Amount desc v) atype pat) -> do
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]
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
-- 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)
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?
txPair
2023-02-12 16:23:32 -05:00
:: MonadFinance m
=> Day
-> AcntID
-> AcntID
-> CurID
-> 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 = []}
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)
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
k <- insert $ TransactionR c d e
2022-12-11 17:51:11 -05:00
mapM_ (insertSplit k) ss
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