Tarkista että maksut ovat tarpeeksi suuria

This commit is contained in:
Saku Laesvuori 2023-11-24 14:15:59 +02:00
parent 72e432cc82
commit 8bd1753539
Signed by: slaesvuo
GPG Key ID: 257D284A2A1D3A32
3 changed files with 40 additions and 32 deletions

View File

@ -1,5 +1,6 @@
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
module Laskutin where module Laskutin where
@ -7,9 +8,9 @@ module Laskutin where
import Control.Concurrent (threadDelay) import Control.Concurrent (threadDelay)
import Control.Monad (forM_, forM) import Control.Monad (forM_, forM)
import Data.Csv (encodeByName) import Data.Csv (encodeByName)
import Data.List.NonEmpty (nonEmpty) import Data.List.NonEmpty (nonEmpty, singleton)
import Data.MIME (Address, renderAddresses) import Data.MIME (Address, renderAddresses)
import Data.Maybe (catMaybes, mapMaybe) import Data.Maybe (fromMaybe, mapMaybe)
import Data.Text.Encoding (decodeUtf8) import Data.Text.Encoding (decodeUtf8)
import System.Exit (exitFailure) import System.Exit (exitFailure)
import System.IO (stderr, hPutStrLn) import System.IO (stderr, hPutStrLn)
@ -31,9 +32,9 @@ main = do
sendInvoicesMain :: FilePath -> SendOptions -> IO () sendInvoicesMain :: FilePath -> SendOptions -> IO ()
sendInvoicesMain csvPath options@SendOptions {email, sendmail, reminders} = do sendInvoicesMain csvPath options@SendOptions {email, sendmail, reminders} = do
invoices <- filter (\(_, Invoice {isPaid}) -> not isPaid) <$> readInvoices csvPath options invoices <- mapMaybe (\(address, invoice) -> (address,) <$> renderEmail reminders [email] address invoice) <$> readInvoices csvPath options
forM_ invoices $ \invoice@(address, _) -> do forM_ invoices $ \(address, invoiceMail) -> do
sendEmail sendmail $ uncurry (renderEmail reminders [email]) invoice sendEmail sendmail invoiceMail
putStr "Lähetetty osoitteeseen: " putStr "Lähetetty osoitteeseen: "
T.putStrLn $ decodeUtf8 $ renderAddresses address T.putStrLn $ decodeUtf8 $ renderAddresses address
threadDelay (3 * 1000 * 1000) threadDelay (3 * 1000 * 1000)
@ -46,17 +47,21 @@ updateTableMain invoiceCsv transactionCsv = do
LBS.writeFile invoiceCsv $ encodeByName headers newInvoices LBS.writeFile invoiceCsv $ encodeByName headers newInvoices
updateInvoices :: [CsvTransaction] -> CsvInvoice -> CsvInvoice updateInvoices :: [CsvTransaction] -> CsvInvoice -> CsvInvoice
updateInvoices transactions invoice@CsvInvoice {isPaid, reference, ..} updateInvoices transactions invoice@CsvInvoice {reference, ..}
| isPaid = invoice | csvIsPaid invoice = invoice
| otherwise = CsvInvoice {isPaid = reference `elem` transactionReferences, ..} | otherwise = CsvInvoice {paid = Just transactionsSum, ..}
where transactionReferences = catMaybes $ transactionReference <$> transactions where transactionsSum = sum $ map amount $ filter hasReference transactions
transactionReference CsvTransaction {referenceOrMessage = Message _} = Nothing hasReference CsvTransaction {referenceOrMessage = Reference ref} = reference == ref
transactionReference CsvTransaction {referenceOrMessage = Reference ref} = Just ref hasReference CsvTransaction {referenceOrMessage = Message _} = False
readInvoices :: FilePath -> SendOptions -> IO [([Address], Invoice)] readInvoices :: FilePath -> SendOptions -> IO [([Address], Invoice)]
readInvoices csv SendOptions {account, recipient, subject, message, due} = do readInvoices csv SendOptions {account, recipient, subject, message, due} = do
(_, csvInvoices) <- parseCsvFile csv (_, csvInvoices) <- parseCsvFile csv
forM csvInvoices $ \CsvInvoice {..} -> do forM csvInvoices $ \csvInvoice@CsvInvoice {..} -> do
invoiceRows <- maybe (hPutStrLn stderr ("No invoice rows in invoice") >> exitFailure) pure $ invoiceRows' <- maybe (hPutStrLn stderr ("No invoice rows in invoice") >> exitFailure) pure $
nonEmpty $ mapMaybe invoiceRowFromCsv rows nonEmpty $ mapMaybe invoiceRowFromCsv rows
let isPaid = fromMaybe 0 paid >= csvInvoiceSum csvInvoice
invoiceRows = case paid of
Nothing -> invoiceRows'
Just euro -> invoiceRows' <> singleton InvoiceRow {name = "Aiemmin maksettu", amount = 1, price = negate euro}
pure ([invoiceRecipient], Invoice { rows = invoiceRows, ..}) pure ([invoiceRecipient], Invoice { rows = invoiceRows, ..})

View File

@ -2,7 +2,7 @@
{-# LANGUAGE DisambiguateRecordFields #-} {-# LANGUAGE DisambiguateRecordFields #-}
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
module Laskutin.CSV (CsvInvoice(..), CsvTransaction(..), TransactionMessage(..), parseCsvFile, invoiceRowFromCsv) where module Laskutin.CSV (CsvInvoice(..), CsvTransaction(..), TransactionMessage(..), csvInvoiceSum, csvInvoiceRowSum, csvIsPaid, parseCsvFile, invoiceRowFromCsv) where
import Data.Bifunctor (bimap, second) import Data.Bifunctor (bimap, second)
import Control.Monad ((>=>), guard) import Control.Monad ((>=>), guard)
@ -29,7 +29,7 @@ import Laskutin.Types
data CsvInvoice = CsvInvoice data CsvInvoice = CsvInvoice
{ invoiceRecipient :: Address { invoiceRecipient :: Address
, reference :: Reference , reference :: Reference
, isPaid :: Bool , paid :: Maybe Euro
, rows :: [CsvInvoiceRow] , rows :: [CsvInvoiceRow]
} deriving Show } deriving Show
@ -47,6 +47,16 @@ data CsvTransaction = CsvTransaction
, amount :: Euro , amount :: Euro
} deriving Show } deriving Show
csvIsPaid :: CsvInvoice -> Bool
csvIsPaid CsvInvoice {paid = Nothing} = False
csvIsPaid invoice@CsvInvoice {paid = Just paid} = paid >= csvInvoiceSum invoice
csvInvoiceSum :: CsvInvoice -> Euro
csvInvoiceSum CsvInvoice {rows} = sum $ csvInvoiceRowSum <$> rows
csvInvoiceRowSum :: CsvInvoiceRow -> Euro
csvInvoiceRowSum CsvInvoiceRow {amount, price} = maybe 0 ((* price) . fromIntegral) amount
data TransactionMessage = Message Text data TransactionMessage = Message Text
| Reference Reference | Reference Reference
deriving Show deriving Show
@ -57,14 +67,6 @@ instance FromField Address where
instance ToField Address where instance ToField Address where
toField = renderAddress toField = renderAddress
instance FromField Bool where
parseField "x" = pure True
parseField _ = pure False
instance ToField Bool where
toField True = "x"
toField False = ""
instance FromField Day where instance FromField Day where
parseField = iso8601ParseM . T.unpack . decodeUtf8 parseField = iso8601ParseM . T.unpack . decodeUtf8
@ -83,14 +85,14 @@ instance FromNamedRecord CsvInvoice where
<*> namedRecordToRows (foldr delete row [emailField, referenceField, paidField]) <*> namedRecordToRows (foldr delete row [emailField, referenceField, paidField])
instance ToNamedRecord CsvInvoice where instance ToNamedRecord CsvInvoice where
toNamedRecord CsvInvoice {invoiceRecipient, reference, isPaid, rows} = namedRecord $ toNamedRecord CsvInvoice {invoiceRecipient, reference, paid, rows} = namedRecord $
[ emailField .= invoiceRecipient [ emailField .= invoiceRecipient
, referenceField .= reference , referenceField .= reference
, paidField .= isPaid , paidField .= paid
] <> (invoiceRowToField <$> rows) ] <> (invoiceRowToField <$> rows)
invoiceRowToField :: CsvInvoiceRow -> (BS.ByteString, BS.ByteString) invoiceRowToField :: CsvInvoiceRow -> (BS.ByteString, BS.ByteString)
invoiceRowToField CsvInvoiceRow {amount, name} = encodeUtf8 name .= amount invoiceRowToField CsvInvoiceRow {amount, price, name} = encodeUtf8 (name <> ":" <> renderEuro price) .= amount
instance FromNamedRecord CsvTransaction where instance FromNamedRecord CsvTransaction where
parseNamedRecord row = CsvTransaction parseNamedRecord row = CsvTransaction
@ -111,7 +113,7 @@ namedRecordToRows = mapM (parseInvoiceRow . bimap decodeUtf8 decodeUtf8) . HM.to
guard $ T.all isDigit x guard $ T.all isDigit x
pure $ read $ T.unpack x pure $ read $ T.unpack x
readHeader x = do readHeader x = do
[name, price'] <- pure $ T.strip <$> T.splitOn ":" x [name, price'] <- pure $ T.splitOn ":" x
price <- readEuro price' price <- readEuro price'
pure (name, price) pure (name, price)

View File

@ -19,11 +19,11 @@ import qualified Data.ByteString.Lazy as LBS
import Laskutin.Types import Laskutin.Types
renderEmail :: Bool -> [Address] -> [Address] -> Invoice -> ByteString renderEmail :: Bool -> [Address] -> [Address] -> Invoice -> Maybe ByteString
renderEmail isReminder senders recipients = renderMessage renderEmail isReminder senders recipients = fmap (renderMessage
. set (headerBCC defaultCharsets) senders . set (headerBCC defaultCharsets) senders
. set (headerTo defaultCharsets) recipients . set (headerTo defaultCharsets) recipients
. set (headerFrom defaultCharsets) senders . set (headerFrom defaultCharsets) senders)
. invoiceEmail isReminder . invoiceEmail isReminder
sendEmail :: FilePath -> ByteString -> IO () sendEmail :: FilePath -> ByteString -> IO ()
@ -36,8 +36,9 @@ sendEmail sendmail email = do
ExitSuccess -> pure () ExitSuccess -> pure ()
ExitFailure code -> throwIO $ ErrorCall ("sendmail exited with error code " <> show code) ExitFailure code -> throwIO $ ErrorCall ("sendmail exited with error code " <> show code)
invoiceEmail :: Bool -> Invoice -> MIMEMessage invoiceEmail :: Bool -> Invoice -> Maybe MIMEMessage
invoiceEmail isReminder Invoice {rows, reference, account, recipient, due, subject, message} = invoiceEmail _ Invoice {isPaid = True} = Nothing
invoiceEmail isReminder Invoice {rows, reference, account, recipient, due, subject, message, isPaid = False} = Just $
set (headerSubject defaultCharsets) (Just $ subjectPrefix <> " " <> subject ) $ createTextPlainMessage $ set (headerSubject defaultCharsets) (Just $ subjectPrefix <> " " <> subject ) $ createTextPlainMessage $
(if isReminder then "Muistutus alla olevan laskun maksamisesta. Jos olet juuri maksanut kyseisen laskun, tämä muistutus on aiheeton.\n\n" else mempty) (if isReminder then "Muistutus alla olevan laskun maksamisesta. Jos olet juuri maksanut kyseisen laskun, tämä muistutus on aiheeton.\n\n" else mempty)
<> (if T.null message then mempty else (message <> "\n\n\n")) <> (if T.null message then mempty else (message <> "\n\n\n"))