Siisti nimistöä

Tämä muutos mm. poistaa tito-osia nimistä, koska ne käyttäjä voi
helposti lisätä tuomalla koko moduulin omaan nimiavaruuteen.
This commit is contained in:
Saku Laesvuori 2025-07-19 15:32:14 +03:00
parent 6b399c80eb
commit 1a9d7e37ae
Signed by: slaesvuo
GPG Key ID: 257D284A2A1D3A32
4 changed files with 113 additions and 108 deletions

View File

@ -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

View File

@ -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

View File

@ -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 {..}

View File

@ -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)