ofx-0.4.0.4/0000755000000000000000000000000012623366261010646 5ustar0000000000000000ofx-0.4.0.4/LICENSE0000644000000000000000000000276212623366261011662 0ustar0000000000000000Copyright (c) 2013, Omari Norman All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Omari Norman nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ofx-0.4.0.4/ofx.cabal0000644000000000000000000000303012623366261012422 0ustar0000000000000000name: ofx version: 0.4.0.4 synopsis: Parser for OFX data description: A parser for Open Financial Exchange (OFX) financial data. This handles OFX version 1.03, which is documented at http://www.ofx.net This also handles QFX, which is OFX plus some minor additions made by Intuit, the publishers of Quicken. . The parser will place all the data into a tree, which you can query for whatever data you may need, although you will need to be familiar with the OFX spec to do this. There is also a Transaction type which you can easily parse from the tree; this will contain most, if not all, of the data you will be interested in if your OFX file is from a bank or credit card account. . All the OFX and QFX files I have seen use the format from the 1.0 series of OFX, which is primarily SGML based. OFX version 2 is XML based. If this parser works on those files, consider it a happy accident. . The OFX spec is available at homepage: http://www.github.com/massysett/ofx license: BSD3 license-file: LICENSE author: Omari Norman maintainer: omari@smileystation.com category: Finance build-type: Simple cabal-version: >=1.8 source-repository head type: git location: git://github.com/massysett/ofx.git library ghc-options: -Wall exposed-modules: Data.OFX build-depends: base >=4.5.0.0 && < 5 , parsec >=3.1.0 , pretty >=1.1.0.0 , time >=1.4 ofx-0.4.0.4/Setup.hs0000644000000000000000000000005612623366261012303 0ustar0000000000000000import Distribution.Simple main = defaultMain ofx-0.4.0.4/Data/0000755000000000000000000000000012623366261011517 5ustar0000000000000000ofx-0.4.0.4/Data/OFX.hs0000644000000000000000000006732312623366261012522 0ustar0000000000000000{-# LANGUAGE OverloadedStrings, FlexibleContexts #-} -- | Parser for downloaded OFX files. -- -- This parser was written based on the OFX version 1.03 -- specification, which is available at -- -- -- -- It will probably work on earlier versions of OFX without -- incident. However, it may or may not not work on newer versions of -- OFX, which are XML based (this version of OFX is SGML based.) -- -- It will also parse QFX files, which are OFX files with minor -- proprietary additions by Intuit, the maker of Quicken. -- -- An OFX file consists of three parts: the HTTP headers (which this -- parser does NOT handle because typically they will not show up in -- files downloaded to disk), the OFX headers, and the OFX data. This -- parser handles the OFX headers and the OFX data. -- -- The parser in this module simply parses the tags and data into a -- tree, which you can manipulate with other functions. Some functions -- are provided to find the transactions in the tree and place them -- into a 'Transaction' type, which is the data you are most likely -- interested in. If you are interested in other data you can query -- the 'Tag' tree for what you need. -- -- For example, to read in the filename given on the command line and -- parse it and print it nicely: -- -- > import System.Environment -- > import Text.Parsec -- > import Text.PrettyPrint -- > import Data.OFX -- > import System.IO -- > import System.Exit -- > -- > main :: IO () -- > main = do -- > filename:[] <- getArgs -- > contents <- readFile filename -- > ofx <- case parse ofxFile filename contents of -- > Left e -> do -- > hPutStrLn stderr . show $ e -- > exitFailure -- > Right g -> return g -- > putStrLn . render . pFile $ ofx -- > putStrLn -- > . render -- > . pExceptional text (pList . map pTransaction) -- > . transactions -- > $ ofx -- > putStrLn . render . pMaybe text . fiName $ ofx -- > putStrLn . render . pMaybe text . accountNumber $ ofx -- module Data.OFX ( -- * Error handling Err -- * The OFX data tree , HeaderTag , HeaderValue , OFXHeader(..) , TagName , TagData , Tag(..) , OFXFile(..) -- * Manipulating the OFX tag tree , find , findPath , tagData , pathData , findData -- * Extracting specific data , fiName , creditCardNumber , bankAccountNumber , accountNumber -- * Types to represent specific OFX data , Transaction(..) , transaction , transactions , TrnType(..) , trnType , Payee(..) , payee , CorrectAction(..) , BankAcctTo(..) , bankAcctTo , CCAcctTo(..) , ccAcctTo , AcctType(..) , acctType , CurrencyData(..) , currencyData , Currency(..) , currency , OrigCurrency(..) , origCurrency -- * Parsec parsers , ofxFile , newline , escChar , header , openingTag , closingTag , tag , date , time , tzOffset -- * Pretty printers , pPayee , pTransaction , pTag , pHeader , pFile , pEither , pMaybe , pList , label , pExceptional ) where -- -- # Imports -- import Control.Applicative (many, optional, (<|>)) import Control.Monad (replicateM) import qualified Data.Time as T import Text.Parsec.String (Parser) import Text.Parsec ( lookAhead, char, manyTill, anyChar, (), eof, try, digit, many1, spaces, string, choice ) import qualified Text.Parsec as P import Data.Maybe (listToMaybe) import Data.Monoid ((<>)) import qualified Data.Monoid as M import Text.PrettyPrint ( Doc, hang, text, sep, vcat, nest, (<+>), ($$), parens, brackets ) -- -- # Error handling -- -- | Error handling. Errors are indicated with a Left String; -- successes with a Right. type Err = Either String -- -- # Data types -- -- | Headers consists of simple @tag:value@ pairs; this represents the -- tag. type HeaderTag = String -- | The value in an OFX header. type HeaderValue = String -- | An OFX file starts with a number of headers, which take the form -- @tag:value@ followed by a newline. These are followed by a blank -- line. data OFXHeader = OFXHeader HeaderTag HeaderValue deriving (Eq, Show) -- | The name of an OFX tag type TagName = String -- | The data accompanying an OFX tag. type TagData = String -- | The main OFX data consists of a series of tags. OFX 1.03 is SGML, -- not XML. This means that opening tags need not have closing -- tags. In OFX, a tag either has data and no child elements, or it -- has no data and it has child elements. data Tag = Tag TagName (Either TagData [Tag]) deriving (Eq, Show) -- | All the data from an OFX file. data OFXFile = OFXFile { fHeader :: [OFXHeader] , fTag :: Tag -- ^ All the data will be contained in a root tag with the TagName -- @OFX@. } deriving (Eq, Show) -- -- # Parsers -- -- | Parses either a UNIX or an MS-DOS newline. According to 1.2.2, -- OFX does not contain any white space between tags. However, since I -- have seen OFX files that do have whitespace between tags, the -- parser makes allowance for this. newline :: Parser () newline = () <$ char '\n' <|> () <$ (char '\r' *> char '\n') "newline" -- | Parses a character, possibly with an escape sequence. The -- greater-than sign, less-than sign, and ampersand must be entered -- with escape sequences. -- -- According to OFX spec section 2.3.2.1, ampersands, less-than signs, -- and greater-than signs must appear as entities. However some banks -- deliver broken OFX files that do not use entities for ampersands -- (and possibly for less-than or greater-than signs too, although I -- have not yet observed such behavior.) There is now an error message -- that reflects this problem. Client code can filter the OFX data -- for known offenders before passing it to this library. escChar :: Parser Char escChar = do c <- anyChar case c of '&' -> do let mkParser (ch, str) = try (ch <$ string str) ent <- choice (map mkParser [('<', "lt"), ('>', "gt"), ('&', "amp")]) ( "entity (\"lt\", \"gt\", or \"amp\")\n" ++ "some banks create broken OFX files. Try\n" ++ "removing the ampersands from the file and\n" ++ "trying again.") _ <- char ';' return ent _ -> return c "character" header :: Parser OFXHeader header = OFXHeader <$> manyTill anyChar (char ':') <* optional (many (char ' ')) <*> manyTill anyChar newline "OFX header" -- | Parses any opening tag. Returns the name of the tag. openingTag :: Parser TagName openingTag = do _ <- char '<' cs <- manyTill escChar (char '>') case cs of [] -> fail "opening tag with empty name" x:_ -> if x == '/' then fail "this is a closing tag" else return cs "opening tag" -- | Parses a closing tag with the given name. closingTag :: TagName -> Parser () closingTag n = do _ <- char '<' _ <- char '/' cs <- manyTill escChar (char '>') if cs == n then return () else fail $ "expecting closing tag named " ++ n ++ "; saw closing tag named " ++ cs "closing tag named " ++ n -- | Parses any tag. The tag itself must be followed by at least one -- character: either the next tag if this is an aggregate tag, or the -- data if this is a data tag. OFX does not allow empty tags. -- -- The OFX spec seems to say that OFX files do not include trailing -- newlines after tags or data, but I have seen these newlines in QFX -- files, so this parses optional trailing newlines and spaces. tag :: Parser Tag tag = do -- try is needed because openingTag will overlap with closingTag n <- try (openingTag <* spaces) children <- many tag if null children then Tag n . Left <$> manyTill escChar (eof <|> lookAhead (() <$ char '<') <|> newline) <* spaces <* optional (try (closingTag n)) <* spaces else Tag n (Right children) <$ spaces <* closingTag n <* spaces "OFX tag" -- | Parses an entire OFX file, including headers. ofxFile :: Parser OFXFile ofxFile = OFXFile <$> manyTill header newline <*> tag <* spaces <* eof "OFX file" -- | Parses an OFX date; provides an error message if the parse fails. parseDate :: String -> Err T.ZonedTime parseDate s = case P.parse date "" s of Left e -> Left $ "could not parse date: " ++ s ++ ": " ++ show e Right g -> return g -- | Parses an OFX date. Fails if the date is not valid or if there is -- no date to be parsed. date :: Parser T.ZonedTime date = do ys <- fmap read $ replicateM 4 digit ms <- fmap read $ replicateM 2 digit ds <- fmap read $ replicateM 2 digit day <- case T.fromGregorianValid ys ms ds of Nothing -> fail $ "invalid date: " ++ show ys ++ "-" ++ show ms ++ "-" ++ show ds Just d -> return d mayTime <- optional time case mayTime of Nothing -> let localTime = T.LocalTime day T.midnight in return $ T.ZonedTime localTime T.utc Just (t, z) -> return $ T.ZonedTime (T.LocalTime day t) z "date" -- | Parses an OFX time. Fails if the time is not valid or if there is -- no time to parse. Fails if there is no time to parse; however, if -- there is a time but no zone, returns the time and UTC for the zone. time :: Parser (T.TimeOfDay, T.TimeZone) time = do h <- fmap read $ replicateM 2 digit m <- fmap read $ replicateM 2 digit s <- fmap read $ replicateM 2 digit (milli, tz) <- do mayDot <- optional (char '.') case mayDot of Nothing -> return (0, T.utc) Just _ -> do mil <- fmap ((/ 1000) . read) $ replicateM 3 digit mayTz <- optional tzOffset case mayTz of Nothing -> return (mil, T.utc) Just t -> return (mil, t) let sec = s + milli return (T.TimeOfDay h m sec, tz) "time" -- | Parses a time zone offset. Fails if there is no time zone offset -- to parse. tzOffset :: Parser T.TimeZone tzOffset = do _ <- char '[' sn <- parseSign whole <- many1 digit mayDot <- optional (char '.') frac <- case mayDot of Nothing -> return "0" Just _ -> many1 digit mayColon <- optional (char ':') name <- case mayColon of Nothing -> return "" Just _ -> many1 P.letter _ <- char ']' let off = sn $ round ((read (whole ++ "." ++ frac)) * (60 :: Double)) return $ T.TimeZone off False name "time zone offset" where parseSign = do mayMinus <- optional (char '-') case mayMinus of Nothing -> do mayPlus <- optional (char '+') return $ case mayPlus of Nothing -> id Just _ -> negate Just _ -> return negate -- -- # Manipulating trees of tags -- -- | Finds child tags with the given name. When a tag is found, that -- tag is not searched for further children with the same name. find :: TagName -> Tag -> [Tag] find n t@(Tag x p) | n == x = [t] | otherwise = case p of Left _ -> [] Right ts -> concatMap (find n) ts -- | Descends through a tree of tags to find a tag at a specific -- location in the tree. Fails if any part of the search fails. For -- example, to find the financial institution ORG tag, where @t@ is -- the root @OFX@ tag: -- -- > findPath ["SIGNONMSGSRSV1", "SONRS", "FI", "ORG"] t findPath :: [TagName] -> Tag -> Maybe Tag findPath [] t = Just t findPath (n:ns) t = case listToMaybe (find n t) of Nothing -> Nothing Just r -> findPath ns r -- | Gets the data from a tag, if it is a tag with data. tagData :: Tag -> Maybe TagData tagData (Tag _ ei) = either return (const Nothing) ei -- | Goes to a certain path in the tag hierarchy and pulls the -- requested data, if the tag is present and it is a data tag. For -- example, to get the name of the financial institution: -- -- > pathData ["SIGNONMSGSRSV1", "SONRS", "FI", "ORG"] f pathData :: [TagName] -> OFXFile -> Maybe TagData pathData p (OFXFile _ t) = findPath p t >>= tagData -- | Gets the name of the financial institution from the FI tag, if -- available. The OFX spec does not require this tag to be present. fiName :: OFXFile -> Maybe TagData fiName = pathData ["SIGNONMSGSRSV1", "SONRS", "FI", "ORG"] -- | Gets the credit card number, if available. The OFX spec does not -- require this tag to be present. creditCardNumber :: OFXFile -> Maybe TagData creditCardNumber = pathData [ "CREDITCARDMSGSRSV1", "CCSTMTTRNRS", "CCSTMTRS", "CCACCTFROM", "ACCTID" ] -- | Gets the bank account number, if available. The OFX spec does not -- require this tag to be present. bankAccountNumber :: OFXFile -> Maybe TagData bankAccountNumber = pathData [ "BANKMSGSRSV1", "STMTTRNRS", "STMTRS", "BANKACCTFROM", "ACCTID" ] -- | Gets either the credit card or bank account number, if available. accountNumber :: OFXFile -> Maybe TagData accountNumber f = creditCardNumber f <|> bankAccountNumber f -- | Finds the first tag (either this tag or any children) that has -- the given name and that is a data tag (not an aggregate tag.) If no -- data tag with the given name is found, returns Nothing. findData :: TagName -> Tag -> Maybe TagData findData n (Tag tn e) = case e of Left d -> if tn == n then Just d else Nothing Right ts -> M.getFirst . M.mconcat . map M.First . map (findData n) $ ts -- | Finds the first tag (either this tag or any children) that has -- the given name and that is a data tag. Gives an error message if -- the tag is not found. required :: TagName -> Tag -> Err TagData required n t = case findData n t of Nothing -> Left $ "required tag missing: " ++ n Just r -> return r -- -- # OFX data -- -- | OFX transaction types. These are used in STMTTRN aggregates, see -- OFX spec section 11.4.2.3.1.1. data TrnType = TCREDIT | TDEBIT | TINT -- ^ Interest earned or paid (which it is depends on sign of amount) | TDIV -- ^ Dividend | TFEE | TSRVCHG | TDEP -- ^ Deposit | TATM -- ^ ATM debit or credit (which it is depends on sign of amount) | TPOS -- ^ Point of sale debit or credit (which it is depends on sign of -- amount) | TXFER -- ^ Transfer | TCHECK | TPAYMENT -- ^ Electronic payment | TCASH -- ^ Cash withdrawal | TDIRECTDEP -- ^ Direct deposit | TDIRECTDEBIT -- ^ Merchant initiated debit | TREPEATPMT -- ^ Repeating payment / standing order | TOTHER deriving (Eq, Ord, Show) -- | A single STMTTRN, see OFX spec section 11.4.2.3.1. This is most -- likely what you are interested in after downloading a statement -- from a bank. data Transaction = Transaction { txTRNTYPE :: TrnType -- ^ Transaction type , txDTPOSTED :: T.ZonedTime -- ^ Date transaction was posted to account , txDTUSER :: Maybe T.ZonedTime -- ^ Date user initiated transaction, if known , txDTAVAIL :: Maybe T.ZonedTime -- ^ Date funds are available , txTRNAMT :: String -- ^ Amount of transaction. This is left as the string that was -- originally in the download. That means the transaction may -- include a plus or minus sign (no sign is the same as a plus -- sign.) According to section 3.2.9.2, amounts are always signed -- from the perspective of the customer. -- -- Typically negative amounts: -- -- * Investment buy amount, investment sell quantity -- -- * Bank statement debit amounts, checks, fees -- -- * Credit card purchases -- -- * Margin balance (unless the institution owes the client money) -- -- Typically positive amounts: -- -- * Investment sell amount, investment buy quantity -- -- * Bank statement credits -- -- * Credit card payments -- -- * Ledger balance (unless the account is overdrawn) -- -- Formats for amounts are described in 3.2.9.1. If there is no -- decimal point, there is an implied decimal point at the end of -- the value. Trailing and leading spaces \"should\" be -- stripped. Positive or minus is indicated with a leading sign; a -- plus sign is assumed if there is no sign. -- -- An amount has a maximum of 32 alphanumeric characters, -- including digits and punctuation. -- -- The radix point is indicated with either a period or a -- comma. Amounts \"should\" not include any digit grouping -- characters. , txFITID :: String -- ^ Transaction ID issued by financial institution. Used to -- detect duplicate downloads. , txCORRECTFITID :: Maybe String -- ^ If present, this indicates the FITID of a previously sent -- transaction that is corrected by this record. This transaction -- replaces or deletes the transaction that it corrects, based on -- the value of CORRECTACTION below. , txCORRECTACTION :: Maybe CorrectAction -- ^ See 'CorrectAction' and 'txCORRECTFITID' , txSRVRTID :: Maybe String -- ^ Server assigned transaction ID; used for transactions -- initiated by client, such as payment or funds transfer , txCHECKNUM :: Maybe String -- ^ Check or other reference number , txREFNUM :: Maybe String -- ^ Reference number that uniquely identifies the -- transaction. Can be used in addition to or instead of a -- CHECKNUM. , txSIC :: Maybe String -- ^ Standard Industrial Code , txPAYEEID :: Maybe String -- ^ Payee identifier if available , txPayeeInfo :: Maybe (Either String Payee) -- ^ Information on the payee. The OFX spec seems to be saying -- that every transaction must have either NAME, wich is \"name of -- payee or description of transaction\", or the Payee -- aggregate. The former is indicated with a Left, the latter with -- a Right. , txAccountTo :: Maybe (Either BankAcctTo CCAcctTo) -- ^ Information on a transfer. If this transaction wa sa -- transfer, this may contain information about the account being -- transferred to. , txMEMO :: Maybe String -- ^ Extra information not in NAME , txCurrency :: Maybe (Either Currency OrigCurrency) -- ^ Currency option. OFX spec says to choose either CURRENCY or -- ORIGCURRENCY. } deriving (Show) data Payee = Payee { peNAME :: String , peADDR1 :: String , peADDR2 :: Maybe String , peADDR3 :: Maybe String , peCITY :: String , peSTATE :: String , pePOSTALCODE :: String , peCOUNTRY :: Maybe String , pePHONE :: String } deriving (Eq, Show) -- | Can be either REPLACE or DELETE. data CorrectAction = REPLACE -- ^ Replaces the transaction referenced by the CORRECTFITID | DELETE -- ^ Deletes the transaction referenced by the CORRECTFITID deriving (Eq, Show, Read) data BankAcctTo = BankAcctTo { btBANKID :: String -- ^ Routing and transit number , btBRANCHID :: Maybe String -- ^ Bank identifier for international banks , btACCTID :: String -- ^ Account number , btACCTTYPE :: AcctType -- ^ Type of account , btACCTKEY :: Maybe String -- ^ Checksum for international banks } deriving Show data CCAcctTo = CCAcctTo { ctACCTID :: String -- ^ Account number , ctACCTKEY :: Maybe String -- ^ Checksum for international banks } deriving (Eq, Show) data AcctType = ACHECKING | ASAVINGS | AMONEYMRKT | ACREDITLINE deriving (Eq, Show, Ord) acctType :: String -> Err AcctType acctType s | s == "CHECKING" = return ACHECKING | s == "SAVINGS" = return ASAVINGS | s == "MONEYMRKT" = return AMONEYMRKT | s == "CREDITLINE" = return ACREDITLINE | otherwise = Left $ "unrecognized account type: " ++ s -- | Holds all data both for CURRENCY and for ORIGCURRENCY. data CurrencyData = CurrencyData { cdCURRATE :: String -- ^ Ratio of CURDEF currency to CURSYM currency, in decimal form , cdCURSYM :: String -- ^ ISO-4217 3-letter currency identifier } deriving (Eq, Show) data Currency = Currency CurrencyData deriving (Eq, Show) data OrigCurrency = OrigCurrency CurrencyData deriving (Eq, Show) -- -- # Helpers to build aggregates -- trnType :: TagData -> Maybe TrnType trnType d = case d of "CREDIT" -> Just TCREDIT "DEBIT" -> Just TDEBIT "INT" -> Just TINT "DIV" -> Just TDIV "FEE" -> Just TFEE "SRVCHG" -> Just TSRVCHG "DEP" -> Just TDEP "ATM" -> Just TATM "POS" -> Just TPOS "XFER" -> Just TXFER "CHECK" -> Just TCHECK "PAYMENT" -> Just TPAYMENT "CASH" -> Just TCASH "DIRECTDEP" -> Just TDIRECTDEP "DIRECTDEBIT" -> Just TDIRECTDEBIT "REPEATPMT" -> Just TREPEATPMT "OTHER" -> Just TOTHER _ -> Nothing -- | Gets a single Transaction from a tag. The tag should be the one -- named STMTTRN. Fails with an error message if any required field -- was not present. transaction :: Tag -> Err Transaction transaction t = do let fromMaybe e = maybe (Left e) Right trntyStr <- required "TRNTYPE" t trnTy <- fromMaybe ("could not parse transaction type: " ++ trntyStr) $ trnType trntyStr dtpStr <- required "DTPOSTED" t dtp <- parseDate dtpStr let mayDtuStr = findData "DTUSER" t dtu <- maybe (return Nothing) (fmap Just . parseDate) mayDtuStr let mayDtAvail = findData "DTAVAIL" t dta <- maybe (return Nothing) (fmap Just . parseDate) mayDtAvail amt <- required "TRNAMT" t fitid <- required "FITID" t let correctFitId = findData "CORRECTFITID" t correctAct <- case findData "CORRECTACTION" t of Nothing -> return Nothing Just d -> maybe (return Nothing) (fromMaybe ("could not parse correct action: " ++ d)) . safeRead $ d let srvrtid = findData "SRVRTID" t checknum = findData "CHECKNUM" t refnum = findData "REFNUM" t sic = findData "SIC" t payeeId = findData "PAYEEID" t let mayPyeInfo = fmap (return . Left) (findData "NAME" t) <|> fmap (fmap Right) (payee t) pyeInfo <- maybe (return Nothing) (fmap Just) mayPyeInfo let mayAcctTo = (fmap (fmap Left) $ bankAcctTo t) <|> (fmap (fmap Right) $ ccAcctTo t) mayCcy = (fmap (fmap Left) $ currency t) <|> (fmap (fmap Right) $ origCurrency t) acctTo <- maybe (return Nothing) (fmap Just) mayAcctTo ccy <- maybe (return Nothing) (fmap Just) mayCcy let memo = findData "MEMO" t return Transaction { txTRNTYPE = trnTy , txDTPOSTED = dtp , txDTUSER = dtu , txDTAVAIL = dta , txTRNAMT = amt , txFITID = fitid , txCORRECTFITID = correctFitId , txCORRECTACTION = correctAct , txSRVRTID = srvrtid , txCHECKNUM = checknum , txREFNUM = refnum , txSIC = sic , txPAYEEID = payeeId , txPayeeInfo = pyeInfo , txAccountTo = acctTo , txMEMO = memo , txCurrency = ccy } -- | Parses a Payee record from its parent tag. payee :: Tag -- ^ The tag which contains the PAYEE tag, if there is one. This -- would typically be a STMTTRN tag. -> Maybe (Err Payee) -- ^ Nothing if there is no PAYEE tag. Just if a PAYEE tag is found, -- with a Left if the tag is lacking a required element, or a -- Right if the tag is successfully parsed. -- -- If there is more than one PAYEE tag, only the first one is -- considered. payee = fmap getPayee . listToMaybe . find "PAYEE" where getPayee t = Payee <$> required "NAME" t <*> required "ADDR1" t <*> pure (findData "ADDR2" t) <*> pure (findData "ADDR3" t) <*> required "CITY" t <*> required "STATE" t <*> required "POSTALCODE" t <*> pure (findData "COUNTRY" t) <*> required "PHONE" t currency :: Tag -> Maybe (Err Currency) currency = fmap (fmap Currency) . fmap currencyData . listToMaybe . find "CURRENCY" origCurrency :: Tag -> Maybe (Err OrigCurrency) origCurrency = fmap (fmap OrigCurrency) . fmap currencyData . listToMaybe . find "ORIGCURRENCY" -- | Parses currency data. currencyData :: Tag -- ^ The tag that contains the data, e.g. CURRENCY or ORIGCURRENCY. -> Err CurrencyData currencyData t = CurrencyData <$> required "CURRATE" t <*> required "CURSYM" t bankAcctTo :: Tag -> Maybe (Err BankAcctTo) bankAcctTo = fmap getTo . listToMaybe . find "BANKACCTTO" where getTo t = BankAcctTo <$> required "BANKID" t <*> pure (findData "BRANCHID" t) <*> required "ACCTID" t <*> (required "ACCTTYPE" t >>= acctType) <*> pure (findData "ACCTKEY" t) ccAcctTo :: Tag -> Maybe (Err CCAcctTo) ccAcctTo = fmap getTo . listToMaybe . find "CCACCTTO" where getTo t = CCAcctTo <$> required "ACCTID" t <*> pure (findData "ACCTKEY" t) safeRead :: Read a => String -> Maybe a safeRead s = case reads s of (x, ""):[] -> Just x _ -> Nothing -- | Pulls all Transactions from a file. Might fail if the OFX file -- does not conform to the specification (or if there are bugs in this -- library.) In case of the former, you can manually parse the -- transaction information yourself using functions like -- 'pathData'. In case of the latter, please send bugreports :-) transactions :: OFXFile -> Err [Transaction] transactions = mapM transaction . find "STMTTRN" . fTag -- -- # Pretty printers -- pPayee :: Payee -> Doc pPayee p = hang "Payee:" 2 ls where ls = sep [ label "Name" (text . peNAME $ p) , label "Addr1" (text . peADDR1 $ p) , label "Addr2" (pMaybe text . peADDR2 $ p) , label "Addr3" (pMaybe text . peADDR3 $ p) , label "City" (text . peCITY $ p) , label "State" (text . peSTATE $ p) , label "Postal" (text . pePOSTALCODE $ p) , label "Country" (pMaybe text . peCOUNTRY $ p) , label "Phone" (text . pePHONE $ p) ] pTransaction :: Transaction -> Doc pTransaction a = hang "Transaction:" 2 ls where ls = sep [ label "TRNTYPE" (text . show . txTRNTYPE $ a) , label "DTPOSTED" (text . show . txDTPOSTED $ a) , label "DTUSER" (text . show . txDTUSER $ a) , label "DTAVAIL" (text . show . txDTAVAIL $ a) , label "TRNAMT" (text . txTRNAMT $ a) , label "FITID" (text . txFITID $ a) , label "CORRECTFITID" (pMaybe text . txCORRECTFITID $ a) , label "CORRECTACTION" (text . show . txCORRECTACTION $ a) , label "SRVRTID" (pMaybe text . txSRVRTID $ a) , label "CHECKNUM" (pMaybe text . txCHECKNUM $ a) , label "REFNUM" (pMaybe text . txREFNUM $ a) , label "SIC" (pMaybe text . txSIC $ a) , label "PAYEEID" (pMaybe text . txPAYEEID $ a) , label "PAYEEINFO" (pMaybe (pEither text (text . show)) . txPayeeInfo $ a) , label "ACCOUNTTO" (pMaybe id . fmap (text . show) . txAccountTo $ a) , label "MEMO" (pMaybe text . txMEMO $ a) , label "CURRENCY" (pMaybe (text . show) . txCurrency $ a) ] pTag :: Tag -> Doc pTag (Tag n ei) = case ei of Left d -> "<" <> text n <> ">" <> text d Right ts -> vcat $ "<" <> text n <> ">" : map (nest 2 . pTag) ts ++ [" text n <> ">"] pHeader :: OFXHeader -> Doc pHeader (OFXHeader t v) = text t <> ": " <> text v pFile :: OFXFile -> Doc pFile (OFXFile hs t) = "OFX file:" $$ nest 2 (vcat [ pList . map pHeader $ hs , mempty , pTag t ]) pEither :: (a -> Doc) -> (b -> Doc) -> Either a b -> Doc pEither fa fb = either (\l -> "Left" <+> parens (fa l)) (\r -> "Right" <+> parens (fb r)) pMaybe :: (a -> Doc) -> Maybe a -> Doc pMaybe f = maybe "Nothing" (\x -> "Just" <+> parens (f x)) pList :: [Doc] -> Doc pList ds = case ds of [] -> "[]" x:[] -> brackets x x:xs -> sep $ hang "[" 2 x : map (\d -> hang "," 2 d) xs ++ [ "]" ] label :: String -> Doc -> Doc label s = hang (text (s ++ ":")) (length s + 2) pExceptional :: (e -> Doc) -> (a -> Doc) -> Either e a -> Doc pExceptional fe fa = either (\e -> hang "Exception:" 2 $ parens (fe e)) (\g -> hang "Success:" 2 $ parens (fa g))