module Penny.Brenner.Import (mode) where import Control.Applicative ((<|>)) import Data.Maybe (mapMaybe) import qualified System.Console.MultiArg as MA import qualified Penny.Brenner.Types as Y import qualified Penny.Brenner.Util as U data Arg = AFitFile String | AAllowNew | AUNumber Integer toFitFile :: Arg -> Maybe String toFitFile a = case a of AFitFile s -> Just s _ -> Nothing toNewUNumber :: [Arg] -> Maybe Integer toNewUNumber as = let f i = case i of { AUNumber x -> Just x; _ -> Nothing } in case mapMaybe f as of [] -> Nothing xs -> Just $ last xs data ImportOpts = ImportOpts { fitFile :: Y.FitFileLocation , allowNew :: Y.AllowNew , parser :: Y.FitFileLocation -> IO (Either String [Y.Posting]) , newUNumber :: Maybe Integer } mode :: Y.Mode mode mayFa = MA.modeHelp "import" -- Mode name help -- Help function (processor mayFa) -- Processing function opts -- Options MA.Intersperse -- Interspersion (return . AFitFile) -- Posarg processor where opts = [ MA.OptSpec ["new"] "n" (MA.NoArg AAllowNew) , MA.OptSpec ["unumber"] "u" . MA.OneArg $ \s -> do i <- MA.reader s return $ AUNumber i ] processor :: Maybe Y.FitAcct -> [Arg] -> IO () processor mayFa as = do fa <- U.getFitAcct mayFa let (dbLoc, prsr) = (Y.dbLocation fa, snd . Y.parser $ fa) loc <- case mapMaybe toFitFile as of [] -> fail "you must provide a postings file to read" x:[] -> return (Y.FitFileLocation x) _ -> fail "you cannot provide more than one postings file to read" let aNew = Y.AllowNew $ any (\a -> case a of { AAllowNew -> True; _ -> False }) as maybeNewU = toNewUNumber as doImport dbLoc (ImportOpts loc aNew prsr maybeNewU) -- | Appends new Amex transactions to the existing list. appendNew :: Maybe Integer -- ^ If Just, this is the new U-number for the first -- transaction. Otherwise, the next U number will be the one that is -- one larger than the current maximum in the database. -> [(Y.UNumber, Y.Posting)] -- ^ Existing transactions -> [Y.Posting] -- ^ New transactions -> Maybe ([(Y.UNumber, Y.Posting)], Int) -- ^ New list, and number of transactions added. Fails if the new U -- number was passed in the first argument and this number is not -- valid. appendNew mu db new = let currFitIds = map (Y.fitId . snd) db isNew p = not (any (== Y.fitId p) currFitIds) newPstgs = filter isNew new mkPair i p = (Y.UNumber i, p) maybeU = nextUNum mu db in fmap (\u -> let newWithU = (zipWith mkPair [u..] newPstgs) in (db ++ newWithU, length newWithU)) maybeU nextUNum :: Maybe Integer -> [(Y.UNumber, Y.Posting)] -> Maybe Integer nextUNum mu db = let defaultU = if null db then Nothing else Just $ ( Y.unUNumber . maximum . map fst $ db) + 1 in case mu of Nothing -> defaultU <|> Just 0 Just u -> case defaultU of Nothing -> if u >= 0 then Just u else Nothing Just du -> if u >= du then Just u else Nothing doImport :: Y.DbLocation -> ImportOpts -> IO () doImport dbLoc os = do txnsOld <- U.loadDb (allowNew os) dbLoc parseResult <- parser os (fitFile os) ins <- case parseResult of Left e -> fail e Right g -> return g (new, len) <- case appendNew (newUNumber os) txnsOld ins of Just r -> return r Nothing -> fail "invalid new U number given." U.saveDb dbLoc new putStrLn $ "imported " ++ show len ++ " new transactions." help :: String -> String help pn = unlines [ "usage: " ++ pn ++ " [global-options] import [local-options] FIT_FILE" , "where FIT_FILE is the file downloaded from the financial" , "institution." , "" , "Local Options:" , "" , "-n, --new - Allows creation of new database. Without this option," , "if the database file is not found, quits with an error." , "" , "-u, --unumber - The first U number assigned will be this number." , "Fails if the number you give is not greater than the largest" , "U number already in the database." , "" , "-h, --help - Show this help." , "" ]