{-# LANGUAGE ImportQualifiedPost #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} module Main where import Codec.Archive.Zip (addEntryToArchive, emptyArchive, fromArchive, toEntry) import Control.Category ((>>>)) import Control.Monad (forM, join) import Control.Monad.IO.Class (liftIO) import Control.Exception (assert) import Data.ByteString (ByteString) import Data.ByteString qualified as ByteString import Data.ByteString.Lazy qualified as Lazy import Data.ByteString.Lazy qualified as ByteString.Lazy import Data.Functor.Compose (Compose(..)) import Data.Map.Lazy qualified as Map import Data.Monoid.Textual (toString) import Data.Monoid.Instances.ByteString.UTF8 (ByteStringUTF8(..)) import Data.String (fromString) import Data.Time.Clock (nominalDiffTimeToSeconds) import Data.Time.Clock.POSIX (getPOSIXTime) import Data.Text (Text) import Data.Text.Lazy qualified as Text.Lazy import Network.HTTP.Types.Status (ok200, internalServerError500, notFound404, unsupportedMediaType415, unprocessableEntity422) import Network.Wai.Middleware.RequestLogger import Network.Wai.Middleware.Static import Network.Wai.Parse (FileInfo (..)) import System.Directory (removeDirectoryRecursive) import System.FilePath.Posix (()) import System.Posix.Temp (mkdtemp) import Text.FDF (parse, serialize) import Text.Read (readMaybe) import Web.Scotty import Paths_canadian_income_tax (getDataDir) import Tax.Canada (completeForms) import Tax.FDF qualified as FDF import Tax.PDFtk (fdf2pdf, pdfFile2fdf) main :: IO () main = scotty 3000 $ do middleware logStdoutDev get "/" $ do setHeader "Content-Type" "text/html; charset=utf-8" file "web/client/build/index.html" get "/about.html" $ do file "web/static/about.html" post "/t1/PDF/:province" $ do provinceCode <- captureParam "province" province <- case readMaybe (Text.Lazy.unpack provinceCode) of Nothing -> status notFound404 >> text ("No such province as " <> provinceCode) >> finish Just p -> pure p pdfFiles <- files dir <- liftIO $ mkdtemp "tax" fdfBytes <- liftIO $ fmap sequenceA $ forM pdfFiles $ \(key, FileInfo name _ content)-> do let path = dir fromUTF8 name Lazy.writeFile path content fdf <- pdfFile2fdf path pure ((,) key <$> fdf) case fdfBytes >>= traverse (traverse $ Lazy.toStrict >>> parse) of Left err -> status unsupportedMediaType415 >> text (fromString err) Right fdfs -> case completeForms province (Map.fromList fdfs) of Left err -> status unprocessableEntity422 >> text (fromString err) Right fdfs' -> do let fdfBytes' = Lazy.fromStrict . serialize <$> fdfs' replaceContent :: (Text, FileInfo Lazy.ByteString) -> IO (Either String (Text, FileInfo Lazy.ByteString)) replaceContent (key, FileInfo name ty _) = case Map.lookup key fdfBytes' of Just c -> ((,) key . FileInfo name ty <$>) <$> fdf2pdf (dir fromUTF8 name) c Nothing -> pure (Left $ "Unknown key " <> show key) pdfFiles' <- liftIO $ traverse replaceContent pdfFiles case sequenceA pdfFiles' of Left err -> do status internalServerError500 >> text (fromString err) Right [(_, FileInfo _ _ pdf)] -> do status ok200 setHeader "Content-Type" "application/pdf" raw pdf Right pdfFiles' -> do now <- liftIO $ round . nominalDiffTimeToSeconds <$> getPOSIXTime let pdfArchive = foldr addPDF emptyArchive pdfFiles' addPDF (_, FileInfo name _ c) = addEntryToArchive (toEntry (fromUTF8 name) now c) status ok200 setHeader "Content-Type" "application/zip" raw (fromArchive pdfArchive) liftIO $ removeDirectoryRecursive dir middleware $ staticPolicy (noDots >-> addBase "web/client/build") fromUTF8 :: ByteString -> String fromUTF8 = toString mempty . ByteStringUTF8