{-|
Various additional validation checks that can be performed on a Journal.
Some are called as part of reading a file in strict mode,
others can be called only via the check command.
-}

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NamedFieldPuns #-}

module Hledger.Data.JournalChecks (
  journalCheckAccounts,
  journalCheckCommodities,
  journalCheckPayees,
  journalCheckPairedConversionPostings,
  journalCheckRecentAssertions,
  journalCheckTags,
  module Hledger.Data.JournalChecks.Ordereddates,
  module Hledger.Data.JournalChecks.Uniqueleafnames,
)
where

import Data.Char (isSpace)
import Data.List.Extra
import Data.Maybe
import qualified Data.Map.Strict as M
import qualified Data.Text as T
import Safe (atMay, lastMay, headMay)
import Text.Printf (printf)

import Hledger.Data.Errors
import Hledger.Data.Journal
import Hledger.Data.JournalChecks.Ordereddates
import Hledger.Data.JournalChecks.Uniqueleafnames
import Hledger.Data.Posting (isVirtual, postingDate, transactionAllTags)
import Hledger.Data.Types
import Hledger.Data.Amount (amountIsZero, amountsRaw, missingamt, amounts)
import Hledger.Data.Transaction (transactionPayee, showTransactionLineFirstPart, partitionAndCheckConversionPostings)
import Data.Time (Day, diffDays)
import Hledger.Utils
import Data.Ord
import Hledger.Data.Dates (showDate)

-- | Check that all the journal's postings are to accounts  with
-- account directives, returning an error message otherwise.
journalCheckAccounts :: Journal -> Either String ()
journalCheckAccounts :: Journal -> Either String ()
journalCheckAccounts Journal
j = (Posting -> Either String ()) -> [Posting] -> Either String ()
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
(a -> m b) -> t a -> m ()
mapM_ Posting -> Either String ()
forall a. PrintfType a => Posting -> Either a ()
checkacct (Journal -> [Posting]
journalPostings Journal
j)
  where
    checkacct :: Posting -> Either a ()
checkacct p :: Posting
p@Posting{paccount :: Posting -> AccountName
paccount=AccountName
a}
      | AccountName
a AccountName -> [AccountName] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` Journal -> [AccountName]
journalAccountNamesDeclared Journal
j = () -> Either a ()
forall a b. b -> Either a b
Right ()
      | Bool
otherwise = a -> Either a ()
forall a b. a -> Either a b
Left (a -> Either a ()) -> a -> Either a ()
forall a b. (a -> b) -> a -> b
$ String
-> String
-> Int
-> AccountName
-> String
-> AccountName
-> AccountName
-> a
forall r. PrintfType r => String -> r
printf ([String] -> String
unlines [
           String
"%s:%d:"
          ,String
"%s"
          ,String
"Strict account checking is enabled, and"
          ,String
"account %s has not been declared."
          ,String
"Consider adding an account directive. Examples:"
          ,String
""
          ,String
"account %s"
          ,String
"account %s    ; type:A  ; (L,E,R,X,C,V)"
          ]) String
f Int
l AccountName
ex (AccountName -> String
forall a. Show a => a -> String
show AccountName
a) AccountName
a AccountName
a
        where
          (String
f,Int
l,Maybe (Int, Maybe Int)
_mcols,AccountName
ex) = Posting -> (String, Int, Maybe (Int, Maybe Int), AccountName)
makePostingAccountErrorExcerpt Posting
p

-- | Check that all the commodities used in this journal's postings have been declared
-- by commodity directives, returning an error message otherwise.
journalCheckCommodities :: Journal -> Either String ()
journalCheckCommodities :: Journal -> Either String ()
journalCheckCommodities Journal
j = (Posting -> Either String ()) -> [Posting] -> Either String ()
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
(a -> m b) -> t a -> m ()
mapM_ Posting -> Either String ()
forall a. PrintfType a => Posting -> Either a ()
checkcommodities (Journal -> [Posting]
journalPostings Journal
j)
  where
    checkcommodities :: Posting -> Either a ()
checkcommodities Posting
p =
      case Posting -> Maybe (AccountName, Bool)
findundeclaredcomm Posting
p of
        Maybe (AccountName, Bool)
Nothing -> () -> Either a ()
forall a b. b -> Either a b
Right ()
        Just (AccountName
comm, Bool
_) ->
          a -> Either a ()
forall a b. a -> Either a b
Left (a -> Either a ()) -> a -> Either a ()
forall a b. (a -> b) -> a -> b
$ String
-> String
-> Int
-> AccountName
-> String
-> AccountName
-> AccountName
-> a
forall r. PrintfType r => String -> r
printf ([String] -> String
unlines [
           String
"%s:%d:"
          ,String
"%s"
          ,String
"Strict commodity checking is enabled, and"
          ,String
"commodity %s has not been declared."
          ,String
"Consider adding a commodity directive. Examples:"
          ,String
""
          ,String
"commodity %s1000.00"
          ,String
"commodity 1.000,00 %s"
          ]) String
f Int
l AccountName
ex (AccountName -> String
forall a. Show a => a -> String
show AccountName
comm) AccountName
comm AccountName
comm
          where
            (String
f,Int
l,Maybe (Int, Maybe Int)
_mcols,AccountName
ex) = Posting
-> (Posting
    -> Transaction -> AccountName -> Maybe (Int, Maybe Int))
-> (String, Int, Maybe (Int, Maybe Int), AccountName)
makePostingErrorExcerpt Posting
p Posting -> Transaction -> AccountName -> Maybe (Int, Maybe Int)
finderrcols
      where
        -- Find the first undeclared commodity symbol in this posting's amount
        -- or balance assertion amount, if any. The boolean will be true if
        -- the undeclared symbol was in the posting amount.
        findundeclaredcomm :: Posting -> Maybe (CommoditySymbol, Bool)
        findundeclaredcomm :: Posting -> Maybe (AccountName, Bool)
findundeclaredcomm Posting{pamount :: Posting -> MixedAmount
pamount=MixedAmount
amt,Maybe BalanceAssertion
pbalanceassertion :: Posting -> Maybe BalanceAssertion
pbalanceassertion :: Maybe BalanceAssertion
pbalanceassertion} =
          case ([AccountName] -> Maybe AccountName
findundeclared [AccountName]
postingcomms, [AccountName] -> Maybe AccountName
findundeclared [AccountName]
assertioncomms) of
            (Just AccountName
c, Maybe AccountName
_) -> (AccountName, Bool) -> Maybe (AccountName, Bool)
forall a. a -> Maybe a
Just (AccountName
c, Bool
True)
            (Maybe AccountName
_, Just AccountName
c) -> (AccountName, Bool) -> Maybe (AccountName, Bool)
forall a. a -> Maybe a
Just (AccountName
c, Bool
False)
            (Maybe AccountName, Maybe AccountName)
_           -> Maybe (AccountName, Bool)
forall a. Maybe a
Nothing
          where
            postingcomms :: [AccountName]
postingcomms = (Amount -> AccountName) -> [Amount] -> [AccountName]
forall a b. (a -> b) -> [a] -> [b]
map Amount -> AccountName
acommodity ([Amount] -> [AccountName]) -> [Amount] -> [AccountName]
forall a b. (a -> b) -> a -> b
$ (Amount -> Bool) -> [Amount] -> [Amount]
forall a. (a -> Bool) -> [a] -> [a]
filter (Bool -> Bool
not (Bool -> Bool) -> (Amount -> Bool) -> Amount -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Amount -> Bool
isIgnorable) ([Amount] -> [Amount]) -> [Amount] -> [Amount]
forall a b. (a -> b) -> a -> b
$ MixedAmount -> [Amount]
amountsRaw MixedAmount
amt
              where
                -- Ignore missing amounts and zero amounts without commodity (#1767)
                isIgnorable :: Amount -> Bool
isIgnorable Amount
a = (AccountName -> Bool
T.null (Amount -> AccountName
acommodity Amount
a) Bool -> Bool -> Bool
&& Amount -> Bool
amountIsZero Amount
a) Bool -> Bool -> Bool
|| Amount
a Amount -> Amount -> Bool
forall a. Eq a => a -> a -> Bool
== Amount
missingamt
            assertioncomms :: [AccountName]
assertioncomms = [Amount -> AccountName
acommodity Amount
a | Just Amount
a <- [BalanceAssertion -> Amount
baamount (BalanceAssertion -> Amount)
-> Maybe BalanceAssertion -> Maybe Amount
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe BalanceAssertion
pbalanceassertion]]
            findundeclared :: [AccountName] -> Maybe AccountName
findundeclared = (AccountName -> Bool) -> [AccountName] -> Maybe AccountName
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Maybe a
find (AccountName -> Map AccountName Commodity -> Bool
forall k a. Ord k => k -> Map k a -> Bool
`M.notMember` Journal -> Map AccountName Commodity
jcommodities Journal
j)

        -- Calculate columns suitable for highlighting the excerpt.
        -- We won't show these in the main error line as they aren't
        -- accurate for the actual data.

        -- Find the best position for an error column marker when this posting
        -- is rendered by showTransaction.
        -- Reliably locating a problem commodity symbol in showTransaction output
        -- is really tricky. Some examples:
        --
        --     assets      "C $" -1 @ $ 2
        --                            ^
        --     assets      $1 = $$1
        --                      ^
        --     assets   [ANSI RED]$-1[ANSI RESET]
        --              ^
        --
        -- To simplify, we will mark the whole amount + balance assertion region, like:
        --     assets      "C $" -1 @ $ 2
        --                 ^^^^^^^^^^^^^^
        -- XXX refine this region when it's easy
        finderrcols :: Posting -> Transaction -> AccountName -> Maybe (Int, Maybe Int)
finderrcols Posting
p' Transaction
t AccountName
txntxt =
          case (Posting -> Bool) -> Transaction -> Maybe Int
transactionFindPostingIndex (Posting -> Posting -> Bool
forall a. Eq a => a -> a -> Bool
==Posting
p') Transaction
t of
            Maybe Int
Nothing     -> Maybe (Int, Maybe Int)
forall a. Maybe a
Nothing
            Just Int
pindex -> (Int, Maybe Int) -> Maybe (Int, Maybe Int)
forall a. a -> Maybe a
Just (Int
amtstart, Int -> Maybe Int
forall a. a -> Maybe a
Just Int
amtend)
              where
                tcommentlines :: Int
tcommentlines = Int -> Int -> Int
forall a. Ord a => a -> a -> a
max Int
0 ([AccountName] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length (AccountName -> [AccountName]
T.lines (AccountName -> [AccountName]) -> AccountName -> [AccountName]
forall a b. (a -> b) -> a -> b
$ Transaction -> AccountName
tcomment Transaction
t) Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
1)
                errrelline :: Int
errrelline = Int
1 Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
tcommentlines Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
pindex   -- XXX doesn't count posting coment lines
                errline :: AccountName
errline = AccountName -> Maybe AccountName -> AccountName
forall a. a -> Maybe a -> a
fromMaybe AccountName
"" (AccountName -> [AccountName]
T.lines AccountName
txntxt [AccountName] -> Int -> Maybe AccountName
forall a. [a] -> Int -> Maybe a
`atMay` (Int
errrellineInt -> Int -> Int
forall a. Num a => a -> a -> a
-Int
1))
                acctend :: Int
acctend = Int
4 Int -> Int -> Int
forall a. Num a => a -> a -> a
+ AccountName -> Int
T.length (Posting -> AccountName
paccount Posting
p') Int -> Int -> Int
forall a. Num a => a -> a -> a
+ if Posting -> Bool
isVirtual Posting
p' then Int
2 else Int
0
                amtstart :: Int
amtstart = Int
acctend Int -> Int -> Int
forall a. Num a => a -> a -> a
+ (AccountName -> Int
T.length (AccountName -> Int) -> AccountName -> Int
forall a b. (a -> b) -> a -> b
$ (Char -> Bool) -> AccountName -> AccountName
T.takeWhile Char -> Bool
isSpace (AccountName -> AccountName) -> AccountName -> AccountName
forall a b. (a -> b) -> a -> b
$ Int -> AccountName -> AccountName
T.drop Int
acctend AccountName
errline) Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
1
                amtend :: Int
amtend = Int
amtstart Int -> Int -> Int
forall a. Num a => a -> a -> a
+ (AccountName -> Int
T.length (AccountName -> Int) -> AccountName -> Int
forall a b. (a -> b) -> a -> b
$ AccountName -> AccountName
T.stripEnd (AccountName -> AccountName) -> AccountName -> AccountName
forall a b. (a -> b) -> a -> b
$ (Char -> Bool) -> AccountName -> AccountName
T.takeWhile (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
/=Char
';') (AccountName -> AccountName) -> AccountName -> AccountName
forall a b. (a -> b) -> a -> b
$ Int -> AccountName -> AccountName
T.drop Int
amtstart AccountName
errline)

-- | Check that all the journal's transactions have payees declared with
-- payee directives, returning an error message otherwise.
journalCheckPayees :: Journal -> Either String ()
journalCheckPayees :: Journal -> Either String ()
journalCheckPayees Journal
j = (Transaction -> Either String ())
-> [Transaction] -> Either String ()
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
(a -> m b) -> t a -> m ()
mapM_ Transaction -> Either String ()
forall a. PrintfType a => Transaction -> Either a ()
checkpayee (Journal -> [Transaction]
jtxns Journal
j)
  where
    checkpayee :: Transaction -> Either a ()
checkpayee Transaction
t
      | AccountName
payee AccountName -> [AccountName] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` Journal -> [AccountName]
journalPayeesDeclared Journal
j = () -> Either a ()
forall a b. b -> Either a b
Right ()
      | Bool
otherwise = a -> Either a ()
forall a b. a -> Either a b
Left (a -> Either a ()) -> a -> Either a ()
forall a b. (a -> b) -> a -> b
$
        String
-> String -> Int -> AccountName -> String -> AccountName -> a
forall r. PrintfType r => String -> r
printf ([String] -> String
unlines [
           String
"%s:%d:"
          ,String
"%s"
          ,String
"Strict payee checking is enabled, and"
          ,String
"payee %s has not been declared."
          ,String
"Consider adding a payee directive. Examples:"
          ,String
""
          ,String
"payee %s"
          ]) String
f Int
l AccountName
ex (AccountName -> String
forall a. Show a => a -> String
show AccountName
payee) AccountName
payee
      where
        payee :: AccountName
payee = Transaction -> AccountName
transactionPayee Transaction
t
        (String
f,Int
l,Maybe (Int, Maybe Int)
_mcols,AccountName
ex) = Transaction
-> (Transaction -> Maybe (Int, Maybe Int))
-> (String, Int, Maybe (Int, Maybe Int), AccountName)
makeTransactionErrorExcerpt Transaction
t Transaction -> Maybe (Int, Maybe Int)
finderrcols
        -- Calculate columns suitable for highlighting the excerpt.
        -- We won't show these in the main error line as they aren't
        -- accurate for the actual data.
        finderrcols :: Transaction -> Maybe (Int, Maybe Int)
finderrcols Transaction
t' = (Int, Maybe Int) -> Maybe (Int, Maybe Int)
forall a. a -> Maybe a
Just (Int
col, Int -> Maybe Int
forall a. a -> Maybe a
Just Int
col2)
          where
            col :: Int
col  = AccountName -> Int
T.length (Transaction -> AccountName
showTransactionLineFirstPart Transaction
t') Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
2
            col2 :: Int
col2 = Int
col Int -> Int -> Int
forall a. Num a => a -> a -> a
+ AccountName -> Int
T.length (Transaction -> AccountName
transactionPayee Transaction
t') Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
1

-- | Check that all the journal's tags (on accounts, transactions, postings..)
-- have been declared with tag directives, returning an error message otherwise.
journalCheckTags :: Journal -> Either String ()
journalCheckTags :: Journal -> Either String ()
journalCheckTags Journal
j = do
  ((AccountName, AccountDeclarationInfo) -> Either String ())
-> [(AccountName, AccountDeclarationInfo)] -> Either String ()
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
(a -> m b) -> t a -> m ()
mapM_ (AccountName, AccountDeclarationInfo) -> Either String ()
forall a.
PrintfType a =>
(AccountName, AccountDeclarationInfo) -> Either a ()
checkaccttags ([(AccountName, AccountDeclarationInfo)] -> Either String ())
-> [(AccountName, AccountDeclarationInfo)] -> Either String ()
forall a b. (a -> b) -> a -> b
$ Journal -> [(AccountName, AccountDeclarationInfo)]
jdeclaredaccounts Journal
j
  (Transaction -> Either String ())
-> [Transaction] -> Either String ()
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
(a -> m b) -> t a -> m ()
mapM_ Transaction -> Either String ()
forall a. PrintfType a => Transaction -> Either a ()
checktxntags  ([Transaction] -> Either String ())
-> [Transaction] -> Either String ()
forall a b. (a -> b) -> a -> b
$ Journal -> [Transaction]
jtxns Journal
j
  where
    checkaccttags :: (AccountName, AccountDeclarationInfo) -> Either a ()
checkaccttags (AccountName
a, AccountDeclarationInfo
adi) = ((AccountName, AccountName) -> Either a ())
-> [(AccountName, AccountName)] -> Either a ()
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
(a -> m b) -> t a -> m ()
mapM_ (AccountName -> Either a ()
forall a. PrintfType a => AccountName -> Either a ()
checkaccttag(AccountName -> Either a ())
-> ((AccountName, AccountName) -> AccountName)
-> (AccountName, AccountName)
-> Either a ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
.(AccountName, AccountName) -> AccountName
forall a b. (a, b) -> a
fst) ([(AccountName, AccountName)] -> Either a ())
-> [(AccountName, AccountName)] -> Either a ()
forall a b. (a -> b) -> a -> b
$ AccountDeclarationInfo -> [(AccountName, AccountName)]
aditags AccountDeclarationInfo
adi
      where
        checkaccttag :: AccountName -> Either a ()
checkaccttag AccountName
tagname
          | AccountName
tagname AccountName -> [AccountName] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [AccountName]
declaredtags = () -> Either a ()
forall a b. b -> Either a b
Right ()
          | Bool
otherwise = a -> Either a ()
forall a b. a -> Either a b
Left (a -> Either a ()) -> a -> Either a ()
forall a b. (a -> b) -> a -> b
$ String
-> String -> Int -> AccountName -> String -> AccountName -> a
forall r. PrintfType r => String -> r
printf String
msg String
f Int
l AccountName
ex (AccountName -> String
forall a. Show a => a -> String
show AccountName
tagname) AccountName
tagname
            where (String
f,Int
l,Maybe (Int, Maybe Int)
_mcols,AccountName
ex) = (AccountName, AccountDeclarationInfo)
-> AccountName
-> (String, Int, Maybe (Int, Maybe Int), AccountName)
makeAccountTagErrorExcerpt (AccountName
a, AccountDeclarationInfo
adi) AccountName
tagname
    checktxntags :: Transaction -> Either a ()
checktxntags Transaction
txn = ((AccountName, AccountName) -> Either a ())
-> [(AccountName, AccountName)] -> Either a ()
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
(a -> m b) -> t a -> m ()
mapM_ (AccountName -> Either a ()
forall a. PrintfType a => AccountName -> Either a ()
checktxntag (AccountName -> Either a ())
-> ((AccountName, AccountName) -> AccountName)
-> (AccountName, AccountName)
-> Either a ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (AccountName, AccountName) -> AccountName
forall a b. (a, b) -> a
fst) ([(AccountName, AccountName)] -> Either a ())
-> [(AccountName, AccountName)] -> Either a ()
forall a b. (a -> b) -> a -> b
$ Transaction -> [(AccountName, AccountName)]
transactionAllTags Transaction
txn
      where
        checktxntag :: AccountName -> Either a ()
checktxntag AccountName
tagname
          | AccountName
tagname AccountName -> [AccountName] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [AccountName]
declaredtags = () -> Either a ()
forall a b. b -> Either a b
Right ()
          | Bool
otherwise = a -> Either a ()
forall a b. a -> Either a b
Left (a -> Either a ()) -> a -> Either a ()
forall a b. (a -> b) -> a -> b
$ String
-> String -> Int -> AccountName -> String -> AccountName -> a
forall r. PrintfType r => String -> r
printf String
msg String
f Int
l AccountName
ex (AccountName -> String
forall a. Show a => a -> String
show AccountName
tagname) AccountName
tagname
            where
              (String
f,Int
l,Maybe (Int, Maybe Int)
_mcols,AccountName
ex) = Transaction
-> (Transaction -> Maybe (Int, Maybe Int))
-> (String, Int, Maybe (Int, Maybe Int), AccountName)
makeTransactionErrorExcerpt Transaction
txn Transaction -> Maybe (Int, Maybe Int)
forall p a. p -> Maybe a
finderrcols
                where
                  finderrcols :: p -> Maybe a
finderrcols p
_txn' = Maybe a
forall a. Maybe a
Nothing
                    -- don't bother for now
                    -- Just (col, Just col2)
                    -- where
                    --   col  = T.length (showTransactionLineFirstPart txn') + 2
                    --   col2 = col + T.length tagname - 1
    declaredtags :: [AccountName]
declaredtags = Journal -> [AccountName]
journalTagsDeclared Journal
j [AccountName] -> [AccountName] -> [AccountName]
forall a. [a] -> [a] -> [a]
++ [AccountName]
builtinTags
    msg :: String
msg = ([String] -> String
unlines [
      String
"%s:%d:"
      ,String
"%s"
      ,String
"Strict tag checking is enabled, and"
      ,String
"tag %s has not been declared."
      ,String
"Consider adding a tag directive. Examples:"
      ,String
""
      ,String
"tag %s"
      ])

-- | Tag names which have special significance to hledger.
builtinTags :: [AccountName]
builtinTags = [
   AccountName
"type"  -- declares an account's type
  ,AccountName
"t"     -- generated by timedot letters notation
  -- optionally generated on periodic transactions and auto postings
  ,AccountName
"generated-transaction"
  ,AccountName
"generated-posting"
  -- used internally, not shown (but queryable)
  ,AccountName
"_generated-transaction"
  ,AccountName
"_generated-posting"
  ,AccountName
"_conversion-matched"
  ]

-- | In each tranaction, check that any conversion postings occur in adjacent pairs.
journalCheckPairedConversionPostings :: Journal -> Either String ()
journalCheckPairedConversionPostings :: Journal -> Either String ()
journalCheckPairedConversionPostings Journal
j =
  (Transaction -> Either String ())
-> [Transaction] -> Either String ()
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
(a -> m b) -> t a -> m ()
mapM_ ([AccountName] -> Transaction -> Either String ()
transactionCheckPairedConversionPostings [AccountName]
conversionaccts) ([Transaction] -> Either String ())
-> [Transaction] -> Either String ()
forall a b. (a -> b) -> a -> b
$ Journal -> [Transaction]
jtxns Journal
j
  where conversionaccts :: [AccountName]
conversionaccts = Journal -> [AccountName]
journalConversionAccounts Journal
j

transactionCheckPairedConversionPostings :: [AccountName] -> Transaction -> Either String ()
transactionCheckPairedConversionPostings :: [AccountName] -> Transaction -> Either String ()
transactionCheckPairedConversionPostings [AccountName]
conversionaccts Transaction
t =
  case Bool
-> [AccountName]
-> [IdxPosting]
-> Either
     AccountName
     ([(IdxPosting, IdxPosting)], ([IdxPosting], [IdxPosting]))
partitionAndCheckConversionPostings Bool
True [AccountName]
conversionaccts ([Int] -> [Posting] -> [IdxPosting]
forall a b. [a] -> [b] -> [(a, b)]
zip [Int
0..] ([Posting] -> [IdxPosting]) -> [Posting] -> [IdxPosting]
forall a b. (a -> b) -> a -> b
$ Transaction -> [Posting]
tpostings Transaction
t) of
    Left AccountName
err -> String -> Either String ()
forall a b. a -> Either a b
Left (String -> Either String ()) -> String -> Either String ()
forall a b. (a -> b) -> a -> b
$ AccountName -> String
T.unpack AccountName
err
    Right ([(IdxPosting, IdxPosting)], ([IdxPosting], [IdxPosting]))
_  -> () -> Either String ()
forall a b. b -> Either a b
Right ()

----------

-- | The number of days allowed between an account's latest balance assertion 
-- and latest posting (7).
maxlag :: Integer
maxlag = Integer
7

-- | Check that accounts with balance assertions have no posting more
-- than maxlag days after their latest balance assertion.
-- Today's date is provided for error messages.
journalCheckRecentAssertions :: Day -> Journal -> Either String ()
journalCheckRecentAssertions :: Day -> Journal -> Either String ()
journalCheckRecentAssertions Day
today Journal
j =
  let acctps :: [[Posting]]
acctps = (Posting -> AccountName) -> [Posting] -> [[Posting]]
forall k a. Eq k => (a -> k) -> [a] -> [[a]]
groupOn Posting -> AccountName
paccount ([Posting] -> [[Posting]]) -> [Posting] -> [[Posting]]
forall a b. (a -> b) -> a -> b
$ (Posting -> AccountName) -> [Posting] -> [Posting]
forall b a. Ord b => (a -> b) -> [a] -> [a]
sortOn Posting -> AccountName
paccount ([Posting] -> [Posting]) -> [Posting] -> [Posting]
forall a b. (a -> b) -> a -> b
$ Journal -> [Posting]
journalPostings Journal
j
  in case ([Posting] -> Maybe String) -> [[Posting]] -> [String]
forall a b. (a -> Maybe b) -> [a] -> [b]
mapMaybe (Day -> [Posting] -> Maybe String
findRecentAssertionError Day
today) [[Posting]]
acctps of
    []         -> () -> Either String ()
forall a b. b -> Either a b
Right ()
    String
firsterr:[String]
_ -> String -> Either String ()
forall a b. a -> Either a b
Left String
firsterr

-- | Do the recentassertions check for one account: given a list of postings to the account,
-- if any of them contain a balance assertion, identify the latest balance assertion,
-- and if any postings are >maxlag days later than the assertion,
-- return an error message identifying the first of them.
-- Postings on the same date will be handled in parse order (hopefully).
findRecentAssertionError :: Day -> [Posting] -> Maybe String
findRecentAssertionError :: Day -> [Posting] -> Maybe String
findRecentAssertionError Day
today [Posting]
ps = do
  let rps :: [Posting]
rps = (Posting -> Down Day) -> [Posting] -> [Posting]
forall b a. Ord b => (a -> b) -> [a] -> [a]
sortOn (Day -> Down Day
forall a. a -> Down a
Data.Ord.Down (Day -> Down Day) -> (Posting -> Day) -> Posting -> Down Day
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Posting -> Day
postingDate) [Posting]
ps
  let ([Posting]
afterlatestassertrps, [Posting]
untillatestassertrps) = (Posting -> Bool) -> [Posting] -> ([Posting], [Posting])
forall a. (a -> Bool) -> [a] -> ([a], [a])
span (Maybe BalanceAssertion -> Bool
forall a. Maybe a -> Bool
isNothing(Maybe BalanceAssertion -> Bool)
-> (Posting -> Maybe BalanceAssertion) -> Posting -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
.Posting -> Maybe BalanceAssertion
pbalanceassertion) [Posting]
rps
  Day
latestassertdate <- Posting -> Day
postingDate (Posting -> Day) -> Maybe Posting -> Maybe Day
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> [Posting] -> Maybe Posting
forall a. [a] -> Maybe a
headMay [Posting]
untillatestassertrps
  let withinlimit :: Day -> Bool
withinlimit Day
date = Day -> Day -> Integer
diffDays Day
date Day
latestassertdate Integer -> Integer -> Bool
forall a. Ord a => a -> a -> Bool
<= Integer
maxlag
  Posting
firsterrorp <- [Posting] -> Maybe Posting
forall a. [a] -> Maybe a
lastMay ([Posting] -> Maybe Posting) -> [Posting] -> Maybe Posting
forall a b. (a -> b) -> a -> b
$ (Posting -> Bool) -> [Posting] -> [Posting]
forall a. (a -> Bool) -> [a] -> [a]
dropWhileEnd (Day -> Bool
withinlimit(Day -> Bool) -> (Posting -> Day) -> Posting -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
.Posting -> Day
postingDate) [Posting]
afterlatestassertrps
  let lag :: Integer
lag = Day -> Day -> Integer
diffDays (Posting -> Day
postingDate Posting
firsterrorp) Day
latestassertdate
  let acct :: AccountName
acct = Posting -> AccountName
paccount Posting
firsterrorp
  let (String
f,Int
l,Maybe (Int, Maybe Int)
_mcols,AccountName
ex) = Posting -> (String, Int, Maybe (Int, Maybe Int), AccountName)
makePostingAccountErrorExcerpt Posting
firsterrorp
  let comm :: AccountName
comm =
        case (Amount -> AccountName) -> [Amount] -> [AccountName]
forall a b. (a -> b) -> [a] -> [b]
map Amount -> AccountName
acommodity ([Amount] -> [AccountName]) -> [Amount] -> [AccountName]
forall a b. (a -> b) -> a -> b
$ MixedAmount -> [Amount]
amounts (MixedAmount -> [Amount]) -> MixedAmount -> [Amount]
forall a b. (a -> b) -> a -> b
$ Posting -> MixedAmount
pamount Posting
firsterrorp of
          [] -> AccountName
""
          (AccountName
t:[AccountName]
_) | AccountName -> Int
T.length AccountName
t Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== Int
1 -> AccountName
t
          (AccountName
t:[AccountName]
_) -> AccountName
t AccountName -> AccountName -> AccountName
forall a. Semigroup a => a -> a -> a
<> AccountName
" "
  String -> Maybe String
forall a. a -> Maybe a
Just (String -> Maybe String) -> String -> Maybe String
forall a b. (a -> b) -> a -> b
$ String -> String
chomp (String -> String) -> String -> String
forall a b. (a -> b) -> a -> b
$ String
-> String
-> Int
-> AccountName
-> Integer
-> AccountName
-> Integer
-> AccountName
-> String
-> AccountName
-> AccountName
-> AccountName
-> String
forall r. PrintfType r => String -> r
printf
    ([String] -> String
unlines [
      String
"%s:%d:",
      String
"%s\n",
      String
"The recentassertions check is enabled, so accounts with balance assertions must",
      String
"have a balance assertion within %d days of their latest posting.",
      String
"In account \"%s\", this posting is %d days later",
      String
"than the last balance assertion, which was on %s.",
      String
"",
      String
"Consider adding a more recent balance assertion for this account. Eg:",
      String
"",
      String
"%s\n    %s    %s0 = %s0  ; (adjust asserted amount)"
      ])
    String
f
    Int
l
    (AccountName -> AccountName
textChomp AccountName
ex)
    Integer
maxlag
    AccountName
acct
    Integer
lag
    (Day -> AccountName
showDate Day
latestassertdate)
    (Day -> String
forall a. Show a => a -> String
show Day
today)
    AccountName
acct
    AccountName
comm
    AccountName
comm

-- -- | Print the last balance assertion date & status of all accounts with balance assertions.
-- printAccountLastAssertions :: Day -> [BalanceAssertionInfo] -> IO ()
-- printAccountLastAssertions today acctassertioninfos = do
--   forM_ acctassertioninfos $ \BAI{..} -> do
--     putStr $ printf "%-30s  %s %s, %d days ago\n"
--       baiAccount
--       (if baiLatestClearedAssertionStatus==Unmarked then " " else show baiLatestClearedAssertionStatus)
--       (show baiLatestClearedAssertionDate)
--       (diffDays today baiLatestClearedAssertionDate)