From 1a9d7e37ae657cd76a01d52dccf114e6e683a4e6 Mon Sep 17 00:00:00 2001 From: Saku Laesvuori Date: Sat, 19 Jul 2025 15:32:14 +0300 Subject: [PATCH] =?UTF-8?q?Siisti=20nimist=C3=B6=C3=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tämä muutos mm. poistaa tito-osia nimistä, koska ne käyttäjä voi helposti lisätä tuomalla koko moduulin omaan nimiavaruuteen. --- README.md | 7 ++- src/Data/TITO.hs | 13 ++--- src/Data/TITO/Parser.hs | 113 ++++++++++++++++++++-------------------- src/Data/TITO/Types.hs | 88 +++++++++++++++---------------- 4 files changed, 113 insertions(+), 108 deletions(-) diff --git a/README.md b/README.md index 5a4d57d..2b6438d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # TITO -Tito on Haskell-kirjasto TITO-muotoisten tiliotteiden lukemiseen. +Tito on Haskell-kirjasto TITO-muotoisten tiliotteiden lukemiseen. Moduuli +`Data.TITO` sisältää ylätason funktiot tiliotteen tulkitsemiseen, +`Data.TITO.Types` määrittää tiliotteen tietorakenteen ja `Data.TITO.Parser` +ohjeet sen tekstimuodon tulkitsemisen. Kaikki moduulien nimet on suunniteltu +tuotavaksi omaan nimiavaruuteensa, mutta vain `Data.TITO.readFile` menee +päällekkäin `Prelude`:n nimen kanssa. ## Asentaminen diff --git a/src/Data/TITO.hs b/src/Data/TITO.hs index df376b5..e399a90 100644 --- a/src/Data/TITO.hs +++ b/src/Data/TITO.hs @@ -8,10 +8,11 @@ import qualified Text.Megaparsec as P import Data.TITO.Parser import Data.TITO.Types -readTITO :: BS.ByteString -> Either ParseErrors TITO -readTITO = P.parse tito "" +decode :: BS.ByteString -> Either ParseErrors AccountStatement +decode = P.parse accountStatement "" -readTITOFile :: FilePath -> IO TITO -readTITOFile fp = - BS.readFile fp >>= - either (throwIO . ErrorCall . P.errorBundlePretty) pure . P.parse tito fp +readFile :: FilePath -> IO AccountStatement +readFile fp = + BS.readFile fp + >>= either (throwIO . ErrorCall . P.errorBundlePretty) pure + . P.parse accountStatement fp diff --git a/src/Data/TITO/Parser.hs b/src/Data/TITO/Parser.hs index 56cc274..e216422 100644 --- a/src/Data/TITO/Parser.hs +++ b/src/Data/TITO/Parser.hs @@ -94,8 +94,8 @@ transactionType = do case n of 1 -> pure Deposit 2 -> pure Withdrawal - 3 -> pure DepositFix - 4 -> pure WithdrawalFix + 3 -> pure DepositCorrection + 4 -> pure WithdrawalCorrection 9 -> pure Declined _ -> P.failure Nothing mempty -- TODO: proper error message @@ -118,46 +118,47 @@ nameSource = do "A" -> pure Customer _ -> P.failure Nothing mempty -- TODO: proper error message -tito :: Parser TITO -tito = TITO <$> titoRoot <*> P.many titoRecord <* P.eof +accountStatement :: Parser AccountStatement +accountStatement = do + tito <- record "00" $ do + P.string "100" -- version number + account <- alphaNumeric 14 + statementNumber <- alphaNumeric 3 + startDate <- date + endDate <- date + created <- timestamp + customer <- alphaNumeric 17 + startBalanceDate <- date + startBalance <- money + _records <- optional' numeric 6 + currency <- optional' alphaNumeric 3 + accountName <- optional' alphaNumeric 30 + accountLimit <- fmap Money <$> optional' numeric 18 + accountOwnerName <- alphaNumeric 35 + bankName <- alphaNumeric 40 + contactInformation <- optional' alphaNumeric 40 + bankSpecific <- optional' alphaNumeric 30 + ibanAndBic <- fmap (fmap $ T.break isSpace) $ optional' alphaNumeric 30 -- FI1234567 XXXXX + return AccountStatement {events = [], ..} + events <- P.many titoRecord <* P.eof + pure $ tito {events} -titoRoot :: Parser TITORoot -titoRoot = record "00" $ do - P.string "100" -- version number - account <- alphaNumeric 14 - statementNumber <- alphaNumeric 3 - startDate <- date - endDate <- date - created <- timestamp - customer <- alphaNumeric 17 - startBalanceDate <- date - startBalance <- money - _records <- optional' numeric 6 - currency <- optional' alphaNumeric 3 - accountName <- optional' alphaNumeric 30 - accountLimit <- fmap Money <$> optional' numeric 18 - accountOwnerName <- alphaNumeric 35 - bankName <- alphaNumeric 40 - contactInformation <- optional' alphaNumeric 40 - bankSpecific <- optional' alphaNumeric 30 - ibanAndBic <- fmap (fmap $ T.break isSpace) $ optional' alphaNumeric 30 -- FI1234567 XXXXX - return TITORoot {..} - -titoRecord :: Parser TITORecord -titoRecord = P.try (transaction False) +titoRecord :: Parser Event +titoRecord = P.try transaction <|> P.try balance <|> P.try summary - <|> P.try fixSummary - <|> P.try special - <|> P.try info - <|> P.try (transaction True) + <|> P.try correctionSummary + <|> P.try bankSpecific + <|> P.try message + <|> P.try transactionNotification -transaction :: Bool -> Parser TITORecord -transaction = fmap . TransactionRecord <*> transaction' 0 +transaction, transactionNotification :: Parser Event +transaction = BasicTransaction <$> transaction' 0 False +transactionNotification = TransactionNotification <$> transaction' 0 True transaction' :: Int -> Bool -> Parser Transaction -transaction' minimumDepth isInformational = do - (depth, transaction) <- record (if isInformational then "80" else "10") $ do +transaction' minimumDepth isNotification = do + (depth, transaction) <- record (if isNotification then "80" else "10") $ do transactionNumber <- numeric 6 archiveId <- optional' alphaNumeric 18 parsedDate <- date @@ -178,13 +179,13 @@ transaction' minimumDepth isInformational = do depth <- fromIntegral <$> numeric 1 <|> const 0 <$> P.char 32 -- space guard $ depth >= minimumDepth return (depth, Transaction {details = [], itemisation = [], date = parsedDate, ..}) - details <- P.many $ P.try $ transactionDetail' isInformational - itemisation <- P.many $ P.try $ transaction' (depth + 1) isInformational + details <- P.many $ P.try $ transactionDetail' isNotification + itemisation <- P.many $ P.try $ transaction' (depth + 1) isNotification pure $ transaction {details, itemisation} transactionDetail' :: Bool -> Parser TransactionDetail -transactionDetail' isInformational = - record' (if isInformational then "81" else "11") $ \recordLength -> do +transactionDetail' isNotification = + record' (if isNotification then "81" else "11") $ \recordLength -> do let detailLength = recordLength - 8 detailType <- optional' alphaNumeric 2 case detailType of @@ -201,7 +202,7 @@ transactionDetail' isInformational = date <- date pure Invoice {..} Just "03" -> Card <$> alphaNumeric 19 <* alphaNumeric 1 <*> optional' alphaNumeric 14 - Just "04" -> Fix <$> alphaNumeric 18 + Just "04" -> Correction <$> alphaNumeric 18 Just "05" -> do foreignAmount <- money alphaNumeric 1 @@ -228,14 +229,14 @@ transactionDetail' isInformational = pure SEPA {..} _ -> Unknown <$> alphaNumeric detailLength -balance :: Parser TITORecord +balance :: Parser Event balance = record "40" $ do date <- date endBalance <- money usableBalance <- optional 19 money pure Balance {..} -summary :: Parser TITORecord +summary :: Parser Event summary = record "50" $ do period <- period date <- date @@ -245,22 +246,22 @@ summary = record "50" $ do withdrawalsTotal <- money pure Summary {..} -fixSummary :: Parser TITORecord -fixSummary = record "51" $ do +correctionSummary :: Parser Event +correctionSummary = record "51" $ do period <- period date <- date - depositFixes <- numeric 8 - depositFixesTotal <- money - withdrawalFixes <- numeric 8 - withdrawalFixesTotal <- money - pure FixSummary {..} + depositCorrections <- numeric 8 + depositCorrectionsTotal <- money + withdrawalCorrections <- numeric 8 + withdrawalCorrectionsTotal <- money + pure CorrectionSummary {..} -special :: Parser TITORecord -special = record' "60" $ \recordLength -> do +bankSpecific :: Parser Event +bankSpecific = record' "60" $ \recordLength -> do BankSpecific <$> alphaNumeric 3 <*> P.takeP Nothing (recordLength - 9) -info :: Parser TITORecord -info = record' "70" $ \recordLength -> do +message :: Parser Event +message = record' "70" $ \recordLength -> do bankId <- alphaNumeric 3 - info <- T.unlines . T.chunksOf 80 <$> alphaNumeric (recordLength - 9) - pure Info {..} + message <- T.unlines . T.chunksOf 80 <$> alphaNumeric (recordLength - 9) + pure Message {..} diff --git a/src/Data/TITO/Types.hs b/src/Data/TITO/Types.hs index ffa9956..a4fa0e2 100644 --- a/src/Data/TITO/Types.hs +++ b/src/Data/TITO/Types.hs @@ -8,14 +8,7 @@ import Data.Text (Text) import Data.Time (Day, LocalTime) import Data.List.NonEmpty (NonEmpty) -data TITO = TITO - { root :: TITORoot - , records :: [TITORecord] - } deriving (Show, Eq) - -newtype Money = Money Integer deriving (Eq, Show) - -data TITORoot = TITORoot -- T00 +data AccountStatement = AccountStatement { account :: Text , statementNumber :: Text , startDate :: Day @@ -32,42 +25,43 @@ data TITORoot = TITORoot -- T00 , contactInformation :: Maybe Text , bankSpecific :: Maybe Text , ibanAndBic :: Maybe (Text, Text) + , events :: [Event] } deriving (Show, Eq) -data TITORecord = TransactionRecord -- T80 if isInformational, else T10 - { isInformational :: Bool - , transaction :: Transaction - } - | Balance -- T40 - { date :: Day - , endBalance :: Money - , usableBalance :: Maybe Money - } - | Summary -- T50 - { period :: Period - , date :: Day - , deposits :: Integer - , depositsTotal :: Money - , withdrawals :: Integer - , withdrawalsTotal :: Money - } - | FixSummary -- T51 - { period :: Period - , date :: Day - , depositFixes :: Integer - , depositFixesTotal :: Money - , withdrawalFixes :: Integer - , withdrawalFixesTotal :: Money - } - | BankSpecific -- T60 - { bankId :: Text - , rawData :: ByteString - } - | Info -- T70 - { bankId :: Text - , info :: Text - } - deriving (Show, Eq) +newtype Money = Money Integer deriving (Eq, Show) + +data Event = BasicTransaction Transaction -- T10 + | TransactionNotification Transaction -- T80 + | Balance -- T40 + { date :: Day + , endBalance :: Money + , usableBalance :: Maybe Money + } + | Summary -- T50 + { period :: Period + , date :: Day + , deposits :: Integer + , depositsTotal :: Money + , withdrawals :: Integer + , withdrawalsTotal :: Money + } + | CorrectionSummary -- T51 + { period :: Period + , date :: Day + , depositCorrections :: Integer + , depositCorrectionsTotal :: Money + , withdrawalCorrections :: Integer + , withdrawalCorrectionsTotal :: Money + } + | BankSpecific -- T60 + { bankId :: Text + , rawData :: ByteString + } + | Message -- T70 + { bankId :: Text + , message :: Text + } + deriving (Show, Eq) data Transaction = Transaction { transactionNumber :: Integer @@ -102,8 +96,8 @@ data TransactionDetail = Freeform (NonEmpty Text) -- 00 { cardNumber :: Text , merchantsReference :: Maybe Text } - | Fix -- 04 - { fixedTransaction :: Text} + | Correction -- 04 + { correctedTransaction :: Text} | ForeignCurrency -- 05 { foreignAmount :: Money , currency :: Text @@ -129,7 +123,11 @@ data TransactionDetail = Freeform (NonEmpty Text) -- 00 | Unknown Text -- all others deriving (Eq, Show) -data TransactionType = Deposit | Withdrawal | DepositFix | WithdrawalFix | Declined deriving (Show, Eq) +data TransactionType = Deposit + | Withdrawal + | DepositCorrection + | WithdrawalCorrection + | Declined deriving (Show, Eq) data Period = Day | Statement | Month | Year deriving (Show, Eq)