AVSneo will no longer have unique AVS CardNo: PersonSearch will return one card per company, all having the same CardNo, but possibly different colors Test shows that FRADrive will handle this just fine, provided the old AVS workaround firing several requests at once remain in place
272 lines
17 KiB
Haskell
272 lines
17 KiB
Haskell
-- SPDX-FileCopyrightText: 2022-25 Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <s.jost@frapor.de>
|
|
--
|
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
|
|
module Utils.Avs where
|
|
|
|
import Import.NoModel
|
|
import Utils.Lens
|
|
|
|
import qualified Data.Set as Set
|
|
import qualified Data.Map as Map
|
|
import qualified Data.Text as Text
|
|
|
|
import Servant
|
|
import Servant.Client
|
|
|
|
#ifndef DEVELOPMENT
|
|
import Servant.Client.Core (requestPath)
|
|
import UnliftIO.Concurrent (threadDelay)
|
|
#endif
|
|
|
|
import Model.Types.Avs
|
|
|
|
|
|
-------------
|
|
-- AVS API --
|
|
-------------
|
|
type AVS = BasicAuth "avs_fradrive" String :> "FraVSMService" :> "v1" :> (AVSPersonSearch :<|> AVSPersonStatus :<|> AVSPersonContact :<|> AVSGetRampLicences :<|> AVSSetRampLicences)
|
|
type AVSPersonSearch = "PersonSearch" :> ReqBody '[JSON] AvsQueryPerson :> Post '[JSON] AvsResponsePerson
|
|
type AVSPersonStatus = "PersonStatus" :> ReqBody '[JSON] AvsQueryStatus :> Post '[JSON] AvsResponseStatus
|
|
type AVSPersonContact = "InfoPersonContact" :> ReqBody '[JSON] AvsQueryContact :> Post '[JSON] AvsResponseContact
|
|
type AVSGetRampLicences = "RampDrivingLicenceInfo" :> ReqBody '[JSON] AvsQueryGetLicences :> Post '[JSON] AvsResponseGetLicences
|
|
type AVSSetRampLicences = "RampDrivingLicence" :> ReqBody '[JSON] AvsQuerySetLicences :> Post '[JSON] AvsResponseSetLicences
|
|
|
|
avsMaxSetLicenceAtOnce :: Int
|
|
avsMaxSetLicenceAtOnce = 80 -- maximum input set size for avsQuerySetLicences as enforced by AVS (<80)
|
|
|
|
avsMaxQueryAtOnce :: Int
|
|
avsMaxQueryAtOnce = 250 -- maximum input set size for avsQueryStatus and avsQueryContact as enforced by AVS (<500)
|
|
|
|
avsMaxQueryDelay :: Int
|
|
avsMaxQueryDelay = 200000 -- microsecond to wait before sending another AVS query
|
|
|
|
|
|
avsApi :: Proxy AVS
|
|
avsApi = Proxy
|
|
|
|
{-
|
|
-- Somehow the GADT-style declaration is not flexible enough to compile at the location of the function call
|
|
data AvsQuery where
|
|
AvsQuery :: { avsQueryPerson :: MonadIO m => AvsQueryPerson -> m (Either ClientError AvsResponsePerson)
|
|
, avsQueryStatus :: MonadIO m => AvsQueryStatus -> m (Either ClientError AvsResponseStatus)
|
|
...
|
|
}
|
|
-> AvsQuery
|
|
-}
|
|
|
|
data AvsQuery = AvsQuery
|
|
{ avsQueryPerson :: forall m. MonadIO m => AvsQueryPerson -> m (Either ClientError AvsResponsePerson)
|
|
, avsQueryStatus :: forall m. MonadIO m => AvsQueryStatus -> m (Either ClientError AvsResponseStatus)
|
|
, avsQueryContact :: forall m. MonadIO m => AvsQueryContact -> m (Either ClientError AvsResponseContact)
|
|
, avsQuerySetLicences :: forall m. MonadIO m => AvsQuerySetLicences -> m (Either ClientError AvsResponseSetLicences)
|
|
-- , avsQueryGetLicences :: forall m. MonadIO m => AvsQueryGetLicences -> m (Either ClientError AvsResponseGetLicences) -- not supported by VSM
|
|
, avsQueryGetAllLicences :: forall m. MonadIO m => m (Either ClientError AvsResponseGetLicences)
|
|
}
|
|
|
|
makeLenses_ ''AvsQuery
|
|
|
|
|
|
-- | AVS/VSM-interface currently only allows GetLicences with query argument ID 0, which means all licences; all other queries yield an empty response
|
|
avsQueryAllLicences :: AvsQueryGetLicences
|
|
avsQueryAllLicences = AvsQueryGetLicences $ AvsObjPersonId avsPersonIdZero
|
|
|
|
|
|
mkAvsQuery :: BaseUrl -> BasicAuthData -> ClientEnv -> AvsQuery
|
|
#ifdef DEVELOPMENT
|
|
mkAvsQuery _ _ _ = AvsQuery
|
|
{ avsQueryPerson = return . Right . fakePerson
|
|
, avsQueryStatus = return . Right . fakeStatus
|
|
, avsQueryContact = return . Right . fakeContact
|
|
, avsQuerySetLicences = \_ -> return . Right $ AvsResponseSetLicences mempty
|
|
, avsQueryGetAllLicences = return . Right $ AvsResponseGetLicences mempty
|
|
}
|
|
where
|
|
fakeCard1 = AvsDataPersonCard True (Just $ fromGregorian 2026 5 1) Nothing AvsCardColorGelb (Set.fromList ['F','R','C']) Nothing Nothing Nothing (Just "Fraport AG") (AvsCardNo "6666") "4"
|
|
fakeCard2 = AvsDataPersonCard False (Just $ fromGregorian 2025 6 2) Nothing AvsCardColorRot (Set.fromList ['F','A' ]) Nothing Nothing Nothing (Just "N*ICE Aircraft Services & Support GmbH") (AvsCardNo "7777") "4" -- AVSneo will report multiple companies using multiple cards with same card no
|
|
fakeCard3 = AvsDataPersonCard True (Just $ fromGregorian 2028 7 3) Nothing AvsCardColorBlau mempty Nothing Nothing Nothing (Just "Fraport Facility Services GmbH") (AvsCardNo "7777") "4"
|
|
fakeCard4 = AvsDataPersonCard True (Just $ fromGregorian 2028 7 3) Nothing AvsCardColorGrün mempty Nothing Nothing Nothing (Just "Vollautomaten GmbH") (AvsCardNo "7777") "4"
|
|
|
|
fakePerson :: AvsQueryPerson -> AvsResponsePerson
|
|
fakePerson =
|
|
let
|
|
sarah = Set.singleton $ AvsDataPerson "Sarah" "Vaupel" Nothing 2 (AvsPersonId 2) $ Set.singleton $ AvsDataPersonCard True Nothing Nothing AvsCardColorRot mempty Nothing Nothing Nothing Nothing (AvsCardNo "424242") "8"
|
|
stephan = Set.singleton $ AvsDataPerson "Stephan" "Barth" Nothing 4 (AvsPersonId 4) $ Set.fromList [fakeCard1, fakeCard2]
|
|
steffen = Set.singleton $ AvsDataPerson "Steffen" "Jost" (Just $ mkAvsInternalPersonalNo "47138") 12345678 (AvsPersonId 12345678) $ Set.fromList [fakeCard2, fakeCard3, fakeCard4]
|
|
sumpfi1 = Set.singleton $ AvsDataPerson "Heribert" "Sumpfmeier" Nothing 12345678 (AvsPersonId 12345678) mempty
|
|
sumpfi2 = Set.singleton $ AvsDataPerson "Heribert" "Sumpfmeier" Nothing 12345678 (AvsPersonId 12345678) $ Set.fromList [fakeCard1, fakeCard2, fakeCard3, fakeCard4]
|
|
sumpfi3 = Set.singleton $ AvsDataPerson "Heribert" "Sumpfmeier" Nothing 12345678 (AvsPersonId 12345678) mempty
|
|
in \case
|
|
AvsQueryPerson{avsPersonQueryCardNo=Just (AvsCardNo "00001234"), avsPersonQueryVersionNo=Just "4"} -> AvsResponsePerson steffen
|
|
AvsQueryPerson{avsPersonQueryCardNo=Just (AvsCardNo "00001234")} -> AvsResponsePerson steffen
|
|
AvsQueryPerson{avsPersonQueryCardNo=Just (AvsCardNo "00009944"), avsPersonQueryVersionNo=Just "4"} -> AvsResponsePerson stephan
|
|
AvsQueryPerson{avsPersonQueryCardNo=Just (AvsCardNo "00003344"), avsPersonQueryVersionNo=Just "1"} -> AvsResponsePerson sarah
|
|
AvsQueryPerson{avsPersonQueryCardNo=Just (AvsCardNo "34")} -> AvsResponsePerson $ steffen <> sarah
|
|
AvsQueryPerson{avsPersonQueryCardNo=Just (AvsCardNo "4") , avsPersonQueryVersionNo=Just "4"} -> AvsResponsePerson $ steffen <> stephan
|
|
AvsQueryPerson{avsPersonQueryCardNo=Just (AvsCardNo "00006666"), avsPersonQueryVersionNo=Just "4"} -> AvsResponsePerson $ sumpfi1 <> sumpfi2 <> sumpfi3
|
|
AvsQueryPerson{avsPersonQueryCardNo=Just (AvsCardNo "00007777"), avsPersonQueryVersionNo=Just "4"} -> AvsResponsePerson $ sumpfi1 <> sumpfi2 <> sumpfi3
|
|
AvsQueryPerson{avsPersonQueryCardNo=Just (AvsCardNo "7777"), avsPersonQueryVersionNo=Just "4"} -> AvsResponsePerson $ sumpfi1 <> sumpfi2 <> sumpfi3
|
|
AvsQueryPerson{avsPersonQueryCardNo=Just (AvsCardNo "00008888"), avsPersonQueryVersionNo=Just "4"} -> AvsResponsePerson $ sumpfi1 <> sumpfi2 <> sumpfi3
|
|
_ -> AvsResponsePerson $ steffen <> sumpfi1
|
|
|
|
fakeStatus :: AvsQueryStatus -> AvsResponseStatus
|
|
fakeStatus (AvsQueryStatus (Set.toList -> (api:_))) = AvsResponseStatus $ Set.singleton $ AvsStatusPerson api $ Set.fromList [fakeCard1, fakeCard2, fakeCard3, fakeCard4]
|
|
fakeStatus _ = AvsResponseStatus mempty
|
|
fakeContact :: AvsQueryContact -> AvsResponseContact
|
|
fakeContact (AvsQueryContact (Set.toList -> ((AvsObjPersonId api):_)))
|
|
| api == AvsPersonId 12345678 = AvsResponseContact $ Set.singleton jost
|
|
| api == AvsPersonId 2 = AvsResponseContact $ Set.singleton vaupel
|
|
| api == AvsPersonId 4 = AvsResponseContact $ Set.singleton barth
|
|
| api == AvsPersonId 12345678 = AvsResponseContact $ Set.singleton heribert
|
|
| api == AvsPersonId 604387 = AvsResponseContact $ Set.singleton heribert
|
|
| api == AvsPersonId 604591 = AvsResponseContact $ Set.singleton heribert
|
|
| otherwise = AvsResponseContact mempty
|
|
where
|
|
heribert = AvsDataContact api (AvsPersonInfo "123123123" "Heribert" "Sumpfmeier" (-1) Nothing (Just "sumpfi@tcs.ifi.lmu.de") Nothing (Just $ AvsInternalPersonalNo "57138"))
|
|
(AvsFirmInfo "Fraport AG" 7 "Fraport" Nothing Nothing Nothing Nothing Nothing Nothing Nothing)
|
|
jost = AvsDataContact api (AvsPersonInfo "12345678" "Steffen" "Jost" 0 Nothing (Just "s.jost@fraport.de") (Just "069-69071706") Nothing)
|
|
(AvsFirmInfo "Fraport AG" 7 "Fraport" Nothing Nothing Nothing Nothing Nothing Nothing Nothing)
|
|
vaupel = AvsDataContact api (AvsPersonInfo "2" "Sarah" "Vaupel" 1 Nothing (Just "sarah.vaupel@uniworx.de") (Just "069-69071706") Nothing)
|
|
(AvsFirmInfo "UniWorX GmbH" 9 "UniWorX" (Just "81929") (Just "München") (Just "Germany") (Just "Somestr. 111") (Just "uniworx@uniworx.de") Nothing Nothing)
|
|
barth = AvsDataContact api (AvsPersonInfo "4" "Stephan" "Barth" 2 Nothing (Just "stephan.barth@uniworx.de") (Just "069-69071706") Nothing)
|
|
(AvsFirmInfo "UniWorX GmbH" 9 "UniWorX" Nothing Nothing Nothing Nothing Nothing (Just "sarah.vaupel@uniworx.de") Nothing)
|
|
fakeContact _ = AvsResponseContact mempty
|
|
#else
|
|
mkAvsQuery baseUrl basicAuth cliEnv = AvsQuery
|
|
{ avsQueryPerson = \q -> if q == def then return $ Right $ AvsResponsePerson mempty else -- prevent empty queries
|
|
liftIO $ catch404toEmpty <$> runClientM (rawQueryPerson q) cliEnv
|
|
, avsQueryStatus = \q -> liftIO $ runClientM (splitQuery rawQueryStatus q) cliEnv
|
|
, avsQueryContact = \q -> liftIO $ runClientM (splitQuery rawQueryContact q) cliEnv
|
|
, avsQuerySetLicences = \q -> liftIO $ runClientM (rawQuerySetLicences q) cliEnv -- NOTE: currently uses setLicencesAvs for splitting to ensure return of correctly set licences
|
|
-- , avsQueryGetLicences = \q -> liftIO $ runClientM (rawQueryGetLicences q) cliEnv
|
|
, avsQueryGetAllLicences = liftIO $ runClientM (rawQueryGetLicences avsQueryAllLicences) cliEnv
|
|
}
|
|
where
|
|
( rawQueryPerson
|
|
:<|> rawQueryStatus
|
|
:<|> rawQueryContact
|
|
:<|> rawQueryGetLicences
|
|
:<|> rawQuerySetLicences ) = client avsApi basicAuth
|
|
catch404toEmpty :: Either ClientError AvsResponsePerson -> Either ClientError AvsResponsePerson
|
|
catch404toEmpty (Left (FailureResponse (requestPath -> (base, _path)) (statusCode . responseStatusCode -> 404)))
|
|
| baseUrl == base = Right $ AvsResponsePerson mempty -- WORKAROUND: AVS server erroneously returns 404 if no matching person could be found in its database!
|
|
catch404toEmpty other = other
|
|
|
|
splitQuery :: (Wrapped a, Wrapped c, Unwrapped a ~ Set b, Monoid (Unwrapped c))
|
|
=> (a -> ClientM c) -> a -> ClientM c
|
|
splitQuery rawQuery q
|
|
| Set.size s <= 0 = return $ view _Unwrapped' mempty -- empty query, retun empty answer
|
|
| avsMaxQueryAtOnce >= Set.size s = rawQuery q
|
|
| otherwise = do
|
|
-- logInfoS "AVS" $ "Splitting large query for input Set " <> tshow (Set.size s) -- would require MonadLogger ClientM
|
|
let (avsid1, avsid2) = Set.splitAt avsMaxQueryAtOnce s
|
|
res1 <- rawQuery $ view _Unwrapped' avsid1
|
|
liftIO $ threadDelay avsMaxQueryDelay
|
|
res2 <- splitQuery rawQuery $ view _Unwrapped' avsid2
|
|
return $ view _Unwrapped' (res1 ^. _Wrapped' <> res2 ^. _Wrapped')
|
|
where
|
|
s = view _Wrapped' q
|
|
#endif
|
|
|
|
-----------------------
|
|
-- Utility Functions -- DEPRECTATED
|
|
-----------------------
|
|
|
|
-- retrieve AvsDataPersonCard with longest validity for a given licence,
|
|
-- first argument is a lower bound for avsDataValidTo, usually current day
|
|
-- Note that avsDataValidTo is Nothing if retrieved via AvsResponseStatus (simply use isJust on result in this case)
|
|
-- getValidLicence :: Maybe Day -> AvsLicence -> Set AvsDataPersonCard -> Maybe AvsDataPersonCard
|
|
-- getValidLicence cutoff licence' cards = Set.lookupMax validLicenceCards
|
|
-- where
|
|
-- licence = licence2char licence'
|
|
-- validLicenceCards = Set.filter cardMatch cards
|
|
-- cardMatch AvsDataPersonCard{..} =
|
|
-- avsDataValid && (avsDataValidTo >= cutoff) && (licence `Set.member` avsDataCardAreas)
|
|
|
|
-- -- DEPRECTATED
|
|
-- getCompanyAddress :: AvsDataPersonCard -> (Maybe Text, Maybe Text, Maybe AvsDataPersonCard)
|
|
-- getCompanyAddress card@AvsDataPersonCard{..}
|
|
-- | Just street <- avsDataStreet
|
|
-- , Just pcode <- avsDataPostalCode
|
|
-- , Just city <- avsDataCity
|
|
-- = (avsDataFirm, Just $ Text.unlines $ mcons avsDataFirm [street, Text.unwords [pcode, city]], Just card)
|
|
-- | isJust avsDataFirm = (avsDataFirm, Nothing, Just card)
|
|
-- | otherwise = (Nothing, Nothing, Nothing)
|
|
|
|
-- -- From a set of card, choose the one with the most complete postal address.
|
|
-- -- Returns company, postal address and the associated card where the address was taken from
|
|
-- guessLicenceAddress :: Set AvsDataPersonCard -> (Maybe Text, Maybe Text, Maybe AvsDataPersonCard)
|
|
-- guessLicenceAddress cards
|
|
-- | Just c <- Set.lookupMax cards
|
|
-- , card <- Set.foldr pickLicenceAddress c cards
|
|
-- = getCompanyAddress card
|
|
-- | otherwise = (Nothing, Nothing, Nothing)
|
|
|
|
-- hasAddress :: AvsDataPersonCard -> Bool
|
|
-- hasAddress AvsDataPersonCard{..} = isJust avsDataStreet && isJust avsDataCity && isJust avsDataPostalCode
|
|
|
|
-- pickLicenceAddress :: AvsDataPersonCard -> AvsDataPersonCard -> AvsDataPersonCard
|
|
-- pickLicenceAddress a b
|
|
-- | Just r <- pickBetter' hasAddress = r -- prefer card with complete address
|
|
-- | Just r <- pickBetter' avsDataValid = r -- prefer valid cards
|
|
-- | Just r <- pickBetter' (Set.member licenceRollfeld . avsDataCardAreas) = r -- prefer 'R' cards
|
|
-- | Just r <- pickBetter' (Set.member licenceVorfeld . avsDataCardAreas) = r -- prefer 'F' cards
|
|
-- | avsDataCardColor a > avsDataCardColor b = a -- prefer Yellow over Green, etc.
|
|
-- | avsDataCardColor a < avsDataCardColor b = b
|
|
-- | avsDataIssueDate a > avsDataIssueDate b = a -- prefer later issue date
|
|
-- | avsDataIssueDate a < avsDataIssueDate b = b
|
|
-- | avsDataValidTo a > avsDataValidTo b = a -- prefer later validto date
|
|
-- | avsDataValidTo a < avsDataValidTo b = b
|
|
-- | Just r <- pickBetter' (isJust . avsDataFirm) = r -- prefer having a firm
|
|
-- | a <= b = b -- respect natural Ord instance
|
|
-- | otherwise = a
|
|
-- where
|
|
-- pickBetter' :: (AvsDataPersonCard -> Bool) -> Maybe AvsDataPersonCard
|
|
-- pickBetter' = pickBetter a b
|
|
-- licenceRollfeld = licence2char AvsLicenceRollfeld
|
|
-- licenceVorfeld = licence2char AvsLicenceVorfeld
|
|
|
|
-- {- Note:
|
|
-- For Semigroup Ordering, (<>) ignores the righthand side except for EQ; this could conveniently be used like so
|
|
-- bestAddress :: AvsDataPersonCard -> AvsDataPersonCard -> Ordering
|
|
-- compare a b = compareBy avsDataValid
|
|
-- <> compareBy avsDataValidTo
|
|
-- <> compareBy avsDataIssueDate
|
|
-- ...
|
|
-- where
|
|
-- compareBy f = compare `on` f a b
|
|
-- -}
|
|
|
|
-- Merges several answers by AvsPersonId, preserving all AvsPersonCards
|
|
mergeByPersonId :: Set AvsDataPerson -> Map AvsPersonId AvsDataPerson -> Map AvsPersonId AvsDataPerson
|
|
mergeByPersonId = flip $ Set.foldr aux
|
|
where
|
|
aux :: AvsDataPerson -> Map AvsPersonId AvsDataPerson -> Map AvsPersonId AvsDataPerson
|
|
aux adp = mergeAvsDataPerson $ catalogueAvsDataPerson adp
|
|
|
|
catalogueAvsDataPerson :: AvsDataPerson -> Map AvsPersonId AvsDataPerson
|
|
catalogueAvsDataPerson adp = Map.singleton (avsPersonPersonID adp) adp
|
|
|
|
mergeAvsDataPerson :: Map AvsPersonId AvsDataPerson -> Map AvsPersonId AvsDataPerson -> Map AvsPersonId AvsDataPerson
|
|
mergeAvsDataPerson = Map.unionWithKey merger
|
|
where
|
|
merger :: AvsPersonId -> AvsDataPerson -> AvsDataPerson -> AvsDataPerson
|
|
merger api pa pb =
|
|
let pickBy' :: Ord b => (a -> b) -> (AvsDataPerson -> a) -> a
|
|
pickBy' f p = pickBy f (p pa) (p pb) -- pickBy f `on` p pa pb
|
|
in AvsDataPerson
|
|
{ avsPersonFirstName = pickBy' Text.length avsPersonFirstName
|
|
, avsPersonLastName = pickBy' Text.length avsPersonLastName
|
|
, avsPersonInternalPersonalNo = pickBy' (maybe 0 length) avsPersonInternalPersonalNo
|
|
, avsPersonPersonNo = pickBy' id avsPersonPersonNo
|
|
, avsPersonPersonID = api -- keys must be identical due to call with insertWithKey
|
|
, avsPersonPersonCards = (Set.union `on` avsPersonPersonCards) pa pb
|
|
}
|
|
|
|
pickBy :: Ord b => (a -> b) -> a -> a -> a
|
|
pickBy f x y | f x >= f y = x
|
|
| otherwise = y
|
|
|
|
|