commit 5d507be0ade78e3f300d331e7e8caf30bccc4b5e
parent 9a084a6b9efc68ded8403335f416cade5c5f4ec5
Author: Gust Leenaars <gl.nlnet@leenaa.rs>
Date: Thu, 8 May 2025 18:50:50 +0200
Add tests for entities
Diffstat:
13 files changed, 512 insertions(+), 27 deletions(-)
diff --git a/README.md b/README.md
@@ -24,7 +24,29 @@ Then install [The Haskell Tool Stack](https://docs.haskellstack.org/en/stable/in
$ nix-shell
```
-## Configuration
+## Stack configuration
+
+Currently the [stack configuration](./stack.yaml) is set for GHC version 9.6.6. If you have a different version installed, that probably is perfectly fine, but you need to change some things.
+
+First, find out what version of GHC you are working with:
+
+```
+ghc --version
+```
+
+Then look up which version of LTS (Long Term Support) Haskell packages matches to your version of GHC. You can find an overview on the [website of Stackage](https://www.stackage.org/).
+
+Change your [stack.yaml](./stack.yaml):
+
+```
+snapshot: lts-22.43
+```
+
+Should become
+
+```
+snapshot: lts-YOUR.VERSION
+```
## Testing
diff --git a/src/KYCheck/Check.hs b/src/KYCheck/Check.hs
@@ -76,8 +76,8 @@ findMatchingEntities config entity ((ssid,ent):ents) =
compareEntity :: Config -> LegalEntity -> Entity -> Float
compareEntity config legal_entity entity =
- foldl1 (+) [ multFloats 300 address_score (removeQuality . removeSSID)
- , multFloats 300 name_score (removeQuality . removeSSID)
+ foldl1 (+) [ multFloats (weight_address config) address_score (removeQuality . removeSSID)
+ , multFloats (weight_name config) name_score (removeQuality . removeSSID)
-- TODO: add additional information to score (phone, email, ...)
-- , multFloats 300 info_score removeSSID
]
diff --git a/src/KYCheck/Config.hs b/src/KYCheck/Config.hs
@@ -99,7 +99,28 @@ checkPoints config = do
, weight_nationality config
]
threshold = threshold_points config
- when (total < threshold) $ do (handleError v . Left) $ KYCheck_InvalidDistribution $ T.pack ("Total points obtainable (" ++ show total ++ ") lower than threshold (" ++ show threshold ++ ")")
+ when (total < threshold)
+ $ do (handleError v . Left)
+ $ KYCheck_InvalidDistribution
+ $ T.pack ("Total points obtainable (" ++ show total ++ ") lower than threshold (" ++ show threshold ++ ")")
+ when (threshold_ratio config < 0 || threshold_ratio config > 1)
+ $ do (handleError v . Left) $ KYCheck_InvalidDistribution "Threshold ratio should be between 0 and 1"
+ when (threshold_confidence config < 0 || threshold_confidence config > 1)
+ $ do (handleError v . Left) $ KYCheck_InvalidDistribution "Threshold confidence should be between 0 and 1"
+ when (threshold_points config < 0)
+ $ do (handleError v . Left) $ KYCheck_InvalidDistribution "Threshold points should be positive"
+ when (perfect_points config < 0)
+ $ do (handleError v . Left) $ KYCheck_InvalidDistribution "Perfect points should be positive"
+ when (weight_address config < 0)
+ $ do (handleError v . Left) $ KYCheck_InvalidDistribution "Address weight should be positive"
+ when (weight_date config < 0)
+ $ do (handleError v . Left) $ KYCheck_InvalidDistribution "Birthdate weight should be positive"
+ when (weight_id config < 0)
+ $ do (handleError v . Left) $ KYCheck_InvalidDistribution "Identification document weight should be positive"
+ when (weight_name config < 0)
+ $ do (handleError v . Left) $ KYCheck_InvalidDistribution "Name weight should be positive"
+ when (weight_nationality config < 0)
+ $ do (handleError v . Left) $ KYCheck_InvalidDistribution "Nationality weight should be positive"
return (total >= threshold)
return bool
diff --git a/src/KYCheck/GLS/Type.hs b/src/KYCheck/GLS/Type.hs
@@ -61,7 +61,7 @@ data LegalEntity = LegalEntity
, id :: Text
-- , id_copy :: FilePath
} deriving (Show, Eq)
---
+
-- data Founder = Founder
-- { full_name :: Text
-- , residential_address :: ResidentialAddress
diff --git a/test/Tests/Check.hs b/test/Tests/Check.hs
@@ -49,20 +49,66 @@ data EntityDistribution = EntityDistribution
testTrustedEntity :: Config -> Bool -> Int -> LegalEntity -> Map Int Entity -> EntityDistribution -> TestTree
testTrustedEntity config verbose number entity sanction_list distribution =
testGroup ("Test trusted entity " ++ show number)
- []
- -- [ testSingleEntity config verbose number entity distribution Nothing
- -- , dontFindEntity config verbose target sanction_list ("individual " ++ show number)
- -- ]
+ [ testSingleEntity config verbose number entity distribution Nothing
+ , dontFindEntity config verbose entity sanction_list ("entity " ++ show number)
+ ]
testSanctionedEntity :: Config -> Bool -> Int -> LegalEntity -> Map Int Entity -> EntityDistribution -> TestTree
testSanctionedEntity config verbose number entity sanction_list distribution =
testGroup ("Test entity " ++ show number)
- []
+ [ testSingleEntity config verbose number entity distribution Nothing
+ , findEntity config verbose number entity distribution sanction_list
+ ]
+
+-- | Find target in sanction list
+findEntity :: Config -> Bool -> Int -> LegalEntity -> EntityDistribution -> Map Int Entity -> TestTree
+findEntity config verbose number entity distribution sanction_list =
+ testCase ("Find target " ++ show number) $ do
+ let score = checkEntity config sanction_list entity
+
+ when verbose $ print score
+ compareScore (e_threshold_confidence distribution) score "Score"
+ assertBool ("Should find target " ++ show number ++ ", but found " ++ (show . map reference) score ++ " instead") (number `elem` (map reference score))
dontFindEntity :: Config -> Bool -> LegalEntity -> Map Int Entity -> String -> TestTree
dontFindEntity config verbose entity sanction_list title =
- testGroup ("Do not find entity " ++ title)
- []
+ testCase ("Do not find " ++ title) $ do
+ let score = checkEntity config sanction_list entity
+
+ when verbose $ print score
+ compareScore Nothing score "Score"
+ assertBool ("Should not find " ++ title ++ ", but found " ++ (show . map reference) score ++ " instead") (score == [])
+
+-- | Test if entity matches entity described in target_SSID.xml
+testSingleEntity :: Config -> Bool -> Int -> LegalEntity -> EntityDistribution -> Maybe (IO (Map Int Entity)) -> TestTree
+testSingleEntity config verbose number legal_entity distribution maybe_entity =
+ testCase ("Find target " ++ show number ++ " in test file") $ do
+ ssl <- case maybe_entity of
+ Just ssl -> ssl
+ Nothing -> do let filepath = "target_" ++ show number ++ ".xml"
+ ssl <- (entities . xmlToSSL) <$> parseSwissSanctionsList filepath
+ assertBool (filepath ++ " should contain only this target") (toList ssl /= [])
+ return ssl
+ let ((ssid, entity):_) = toList ssl
+
+
+ assertBool ("SSID should be " ++ show number) (ssid == number)
+ let
+ address_score = multFloats (weight_address config) (checkAddress config (entity_addresses entity) (address legal_entity)) (removeQuality . removeSSID)
+ name_score = multFloats (weight_name config) (checkNames config (entity_names entity) (company_name legal_entity)) (removeQuality . removeSSID)
+
+ total_score = checkEntity config ssl legal_entity
+
+ when verbose $ print total_score
+ comparePoints (e_threshold_address distribution) address_score "Address"
+ comparePoints (e_threshold_name distribution) name_score "Name"
+
+ compareScore (e_threshold_confidence distribution) total_score "Total score"
+
+
+
+
+
-- | Test trusted individual (found in target_SSID.xml, not found in sanction list)
testTrustedIndividual :: Config -> Bool -> Int -> NaturalPerson -> Map Int Individual -> IndividualDistribution -> TestTree
@@ -159,6 +205,11 @@ testSingleIndividual config verbose number target distribution ssl_target =
compareScore (threshold_confidence distribution) total_score "Total score"
+
+
+
+
+
-- | Compare expected points to outcome
comparePoints :: Maybe Float -> Float -> String -> Assertion
comparePoints maybe_threshold points title =
@@ -166,7 +217,6 @@ comparePoints maybe_threshold points title =
Just pts -> assertBool (title ++ " should return >= " ++ show pts ++ " points, but got " ++ show points ++ " instead") (points >= pts)
Nothing -> assertBool (title ++ " should return 0 points, but got " ++ show points ++ " instead") (points == 0 )
-
-- | Compare expected score to real score
compareScore :: Maybe Float -> [Score] -> String -> Assertion
compareScore maybe_threshold score title =
@@ -175,6 +225,10 @@ compareScore maybe_threshold score title =
Nothing -> assertBool (title ++ " should return not matches , but got " ++ show score ++ " instead") (score == [])
+
+
+
+
entityTests :: Config -> Map Int Entity -> TestTree
entityTests config sanction_list =
let
@@ -184,20 +238,30 @@ entityTests config sanction_list =
in
testGroup "Entities"
[ testGroup "Known Sanctioned"
- {- testSE SSID target_SSID $ distribution ADDRESS NAME CONFIDENCE
- MAX_SCORE 150 125 0.75 -}
- [
+ {- testSE SSID target_SSID $ distribution ADDRESS NAME CONFIDENCE
+ MAX_SCORE 150 125 0.75 -}
+ [ testSE 5818 target_5818 $ distribution 0 125 0.75
+ , testSE 5932 target_5932 $ distribution 0 125 0.75
+ , testSE 5842 target_5842 $ distribution 0 125 0.75
+ , testSE 5805 target_5805 $ distribution 125 125 0.75
+ , testSE 44312 target_44312 $ distribution 125 125 0.75
]
, testGroup "Fake entity with XML file"
- {- testTE SSID target_SSID $ distribution ADDRESS NAME CONFIDENCE
- MAX_SCORE 150 125 0.75 -}
- [
+ {- testTE SSID trusted_target_SSID $ distribution ADDRESS NAME CONFIDENCE
+ MAX_SCORE 150 125 0.75 -}
+ [ testTE 1 trusted_entity_6 $ distribution 150 125 0.75
]
, testGroup "Trusted entity"
- {- testSE SSID target_SSID TITLE -}
- [
+ {- dontFindE target_SSID TITLE -}
+ [ dontFindE trusted_entity_1 "NLnet"
+ , dontFindE trusted_entity_2 "Slagharen"
+ , dontFindE trusted_entity_3 "Taler"
+ , dontFindE trusted_entity_4 "Real company"
+ , dontFindE trusted_entity_5 "Legitimate business"
]
]
+ where distribution addr name conf = EntityDistribution (toMaybe addr) (toMaybe name) (toMaybe conf)
+ toMaybe float = case float of 0 -> Nothing; f -> Just f
-- | Test individuals
personTests :: Config -> Map Int Individual -> TestTree
diff --git a/test/Tests/Targets/Entities/Sanctioned.hs b/test/Tests/Targets/Entities/Sanctioned.hs
@@ -6,11 +6,14 @@
{-# LANGUAGE OverloadedStrings #-}
module Tests.Targets.Entities.Sanctioned
- (
+ ( target_5818
+ , target_5932
+ , target_5842
+ , target_5805
+ , target_44312
) where
import Data.CountryCodes
-import Data.Time.Calendar
import KYCheck.GLS.Type as GLS
@@ -33,3 +36,103 @@ import Prelude hiding (id)
-- , town_location =
-- , town_district =
-- , country_subdivision =
+
+target_5818 :: LegalEntity
+target_5818 = LegalEntity
+ { company_name = "LLC Delovaya Set"
+ , contact_person_name = Just "Vladimir Peftiev"
+ , phone = Nothing
+ , email = Nothing
+ , id = "012345ABCDEF"
+ , address = GLS.Address { GLS.country = RU
+ , street_name = "Non existent"
+ , street_number = "Non existent"
+ , GLS.lines = Nothing
+ , building_name = Nothing
+ , building_number = Nothing
+ , zipcode = "Non existent"
+ , town_location = Nothing
+ , town_district = Nothing
+ , country_subdivision = Nothing
+ }
+ }
+
+target_5932 :: LegalEntity
+target_5932 = LegalEntity
+ { company_name = "СООО Юнивест-СтройИнвест"
+ , contact_person_name = Nothing
+ , phone = Nothing
+ , email = Nothing
+ , id = "012345ABCDEF"
+ , address = GLS.Address { GLS.country = RU
+ , street_name = "Non existent"
+ , street_number = "Non existent"
+ , GLS.lines = Nothing
+ , building_name = Nothing
+ , building_number = Nothing
+ , zipcode = "Non existent"
+ , town_location = Nothing
+ , town_district = Nothing
+ , country_subdivision = Nothing
+ }
+ }
+
+target_5842 :: LegalEntity
+target_5842 = LegalEntity
+ { company_name = "Spetspriborservice"
+ , contact_person_name = Nothing
+ , phone = Nothing
+ , email = Nothing
+ , id = "012345ABCDEF"
+ , address = GLS.Address { GLS.country = RU
+ , street_name = "Non existent"
+ , street_number = "Non existent"
+ , GLS.lines = Nothing
+ , building_name = Nothing
+ , building_number = Nothing
+ , zipcode = "Non existent"
+ , town_location = Nothing
+ , town_district = Nothing
+ , country_subdivision = Nothing
+ }
+ }
+
+target_5805 :: LegalEntity
+target_5805 = LegalEntity
+ { company_name = "Beltechexport"
+ , contact_person_name = Nothing
+ , phone = Just "+375172636383"
+ , email = Nothing
+ , id = "012345ABCDEF"
+ , address = GLS.Address { GLS.country = BY
+ , street_name = "Nezavisimost avenue"
+ , street_number = "86B"
+ , GLS.lines = Nothing
+ , building_name = Nothing
+ , building_number = Nothing
+ , zipcode = "220012"
+ , town_location = Nothing
+ , town_district = Nothing
+ , country_subdivision = Nothing
+ }
+ }
+
+target_44312 :: LegalEntity
+target_44312 = LegalEntity
+ { company_name = "Dana Holdings"
+ , contact_person_name = Nothing
+ , phone = Just "+375296362391"
+ , email = Just "info@bir.by"
+ , id = "012345ABCDEF"
+ , address = GLS.Address { GLS.country = BY
+ , street_name = "Peter Mstislavets St"
+ , street_number = "9"
+ , GLS.lines = Nothing
+ , building_name = Nothing
+ , building_number = Nothing
+ , zipcode = "220076"
+ , town_location = Just "Minsk"
+ , town_district = Nothing
+ , country_subdivision = Nothing
+ }
+ }
diff --git a/test/Tests/Targets/Entities/Trusted.hs b/test/Tests/Targets/Entities/Trusted.hs
@@ -6,11 +6,15 @@
{-# LANGUAGE OverloadedStrings #-}
module Tests.Targets.Entities.Trusted
- (
+ ( trusted_entity_1
+ , trusted_entity_2
+ , trusted_entity_3
+ , trusted_entity_4
+ , trusted_entity_5
+ , trusted_entity_6
) where
import Data.CountryCodes
-import Data.Time.Calendar
import KYCheck.GLS.Type as GLS
@@ -37,8 +41,8 @@ import Prelude hiding (id)
-- }
-- }
-target :: LegalEntity
-target = LegalEntity
+trusted_entity_1 :: LegalEntity
+trusted_entity_1 = LegalEntity
{ company_name = "NLnet"
, contact_person_name = Just "Bob Goudriaan"
, phone = Just "+31 (0)20 8884252"
@@ -56,3 +60,103 @@ target = LegalEntity
, country_subdivision = Just "Zuid-Holland"
}
}
+
+trusted_entity_2 :: LegalEntity
+trusted_entity_2 = LegalEntity
+ { company_name = "Attractie- & Vakantiepark Slagharen"
+ , contact_person_name = Nothing
+ , phone = Just "0523-683000"
+ , email = Just "info@slagharen.com"
+ , id = "012345ABCDEF"
+ , address = GLS.Address { GLS.country = NL
+ , street_name = "Zwarte Dijk"
+ , street_number = "37"
+ , GLS.lines = Nothing
+ , building_name = Nothing
+ , building_number = Nothing
+ , zipcode = "7776PB"
+ , town_location = Just "Slagharen"
+ , town_district = Just "Hardenberg"
+ , country_subdivision = Just "Overijssel"
+ }
+ }
+
+trusted_entity_3 :: LegalEntity
+trusted_entity_3 = LegalEntity
+ { company_name = "Taler Systems"
+ , contact_person_name = Nothing
+ , phone = Just "+41 786 92 68 94"
+ , email = Nothing
+ , id = "012345ABCDEF"
+ , address = GLS.Address { GLS.country = LU
+ , street_name = "Rue de Mondorf"
+ , street_number = "7"
+ , GLS.lines = Nothing
+ , building_name = Nothing
+ , building_number = Nothing
+ , zipcode = "L-5421"
+ , town_location = Just "Erpeldange"
+ , town_district = Nothing
+ , country_subdivision = Just "Luxembourg"
+ }
+ }
+
+trusted_entity_4 :: LegalEntity
+trusted_entity_4 = LegalEntity
+ { company_name = "Totally real company"
+ , contact_person_name = Nothing
+ , phone = Nothing
+ , email = Nothing
+ , id = "012345ABCDEF"
+ , address = GLS.Address { GLS.country = BE
+ , street_name = "Non existent"
+ , street_number = "Non existent"
+ , GLS.lines = Nothing
+ , building_name = Nothing
+ , building_number = Nothing
+ , zipcode = "Non existent"
+ , town_location = Nothing
+ , town_district = Nothing
+ , country_subdivision = Nothing
+ }
+ }
+
+trusted_entity_5 :: LegalEntity
+trusted_entity_5 = LegalEntity
+ { company_name = "Legitimate business"
+ , contact_person_name = Nothing
+ , phone = Nothing
+ , email = Nothing
+ , id = "012345ABCDEF"
+ , address = GLS.Address { GLS.country = DE
+ , street_name = "Non existent"
+ , street_number = "Non existent"
+ , GLS.lines = Nothing
+ , building_name = Nothing
+ , building_number = Nothing
+ , zipcode = "Non existent"
+ , town_location = Nothing
+ , town_district = Nothing
+ , country_subdivision = Nothing
+ }
+ }
+
+trusted_entity_6 :: LegalEntity
+trusted_entity_6 = LegalEntity
+ { company_name = "TU Eindhoven"
+ , contact_person_name = Nothing
+ , phone = Just "+31 (0)40 247 9111"
+ , email = Just "info@tue.nl"
+ , id = "012345ABCDEF"
+ , address = GLS.Address { GLS.country = NL
+ , street_name = "Groene Loper"
+ , street_number = "3"
+ , GLS.lines = Nothing
+ , building_name = Nothing
+ , building_number = Nothing
+ , zipcode = "5612AE"
+ , town_location = Just "Eindhoven"
+ , town_district = Nothing
+ , country_subdivision = Just "Noord-Brabant"
+ }
+ }
diff --git a/test/data/target_1.xml b/test/data/target_1.xml
@@ -0,0 +1,35 @@
+<!--
+SPDX-FileCopyrightText: 2025 LNRS
+
+SPDX-License-Identifier: AGPL-3.0-or-later
+SPDX-License-Identifier: EUPL-1.2
+-->
+
+<swiss-sanctions-list list-type="whole-list" date="2024-07-30">
+ <target ssid="11">
+ <sanctions-set-id>1234</sanctions-set-id>
+ <entity>
+ <identity ssid="1" main="true">
+ <name ssid="10" name-type="primary-name" quality="good" lang="eng">
+ <name-part order="1" name-part-type="whole-name">
+ <value>Eindhoven University of Technology</value>
+ </name-part>
+ </name>
+ <name ssid="1000" name-type="alias" quality="good" lang="eng">
+ <name-part order="1" name-part-type="whole-name">
+ <value>TU Eindhoven</value>
+ </name-part>
+ </name>
+ <address ssid="100" place-id="101" quality="good">
+ <address-details>Groene Loper 3</address-details>
+ <zip-code>5612AE</zip-code>
+ </address>
+ </identity>
+ <other-information ssid="110">info@tue.nl</other-information>
+ </entity>
+ </target>
+ <place ssid="101">
+ <location>Eindhoven</location>
+ <country iso-code="NL">Netherlands</country>
+ </place>
+</swiss-sanctions-list>
diff --git a/test/data/target_44312.xml b/test/data/target_44312.xml
@@ -0,0 +1,36 @@
+<!--
+SPDX-FileCopyrightText: 2025 LNRS
+
+SPDX-License-Identifier: AGPL-3.0-or-later
+SPDX-License-Identifier: EUPL-1.2
+-->
+
+<swiss-sanctions-list list-type="whole-list" date="2024-07-30">
+ <target ssid="44306">
+ <sanctions-set-id>4387</sanctions-set-id>
+ <entity>
+ <identity ssid="44312" main="true">
+ <name ssid="49902" name-type="primary-name" quality="good" lang="eng">
+ <name-part order="1" name-part-type="whole-name">
+ <value>Dana Holdings</value>
+ <spelling-variant lang="BEL" script="CYRL" spelling-variant-type="not-defined">ТАА “Дана Холдынгз”</spelling-variant>
+ <spelling-variant lang="RUS" script="CYRL" spelling-variant-type="not-defined">ООО “Дана Холдингз”</spelling-variant>
+ </name-part>
+ </name>
+ <address ssid="49901" place-id="319" quality="good">
+ <address-details>Peter Mstislavets St. 9, pom. 3 (office 4)</address-details>
+ <zip-code>220076</zip-code>
+ </address>
+ </identity>
+ <justification ssid="49896">Dana Holdings is one of the main real estate developers and constructors in Belarus. The company and its subsidiaries received development rights for plots of land and developed several large residential complexes and business centres. Individuals reportedly representing Dana Holdings maintain close relations with President Lukashenka. Liliya Lukashenka, daughter-in-law of the President, had a high-ranking position in Dana Astra. Dana Holdings is still active economically in Belarus. Dana Holdings is therefore benefitting from and supporting the Lukashenka regime.</justification>
+ <other-information ssid="49897">Registration number: 690611860</other-information>
+ <other-information ssid="49898">Websites: https://bir.by/; https://en.dana-holdings.com; https://dana-holdings.com/</other-information>
+ <other-information ssid="49899">E-mail address: info@bir.by</other-information>
+ <other-information ssid="49900">Tel.: +375 (29) 636-23-91</other-information>
+ </entity>
+ </target>
+ <place ssid="319">
+ <location>Minsk</location>
+ <country iso-code="BY">Belarus</country>
+ </place>
+</swiss-sanctions-list>
diff --git a/test/data/target_5805.xml b/test/data/target_5805.xml
@@ -0,0 +1,29 @@
+<!--
+SPDX-FileCopyrightText: 2025 LNRS
+
+SPDX-License-Identifier: AGPL-3.0-or-later
+SPDX-License-Identifier: EUPL-1.2
+-->
+
+<swiss-sanctions-list list-type="whole-list" date="2024-07-30">
+ <target ssid="5802">
+ <sanctions-set-id>4387</sanctions-set-id>
+ <entity>
+ <identity ssid="5805" main="true">
+ <name ssid="5807" name-type="primary-name" quality="good" lang="eng">
+ <name-part order="1" name-part-type="whole-name">
+ <value>Beltechexport</value>
+ <spelling-variant lang="RUS" script="CYRL" spelling-variant-type="not-defined">ЗАО Белтехэкспорт</spelling-variant>
+ </name-part>
+ </name>
+ <address ssid="5806" place-id="4406" quality="good">
+ <address-details>Nezavisimost ave., 86-B</address-details>
+ <zip-code>220012</zip-code>
+ <remark>Tel: (+375 17) 263-63-83, Fax: (+375 17) 263-90-12</remark>
+ </address>
+ </identity>
+ <justification ssid="29565">Beltechexport benefits from the regime as a main exporter of arms and military equipment in Belarus, which requires authorisation from the Belarusian authorities.</justification>
+ <other-information ssid="33238">Financial sanctions according to article 1 do not apply until 15 March 2016.</other-information>
+ </entity>
+ </target>
+</swiss-sanctions-list>
diff --git a/test/data/target_5818.xml b/test/data/target_5818.xml
@@ -0,0 +1,24 @@
+<!--
+SPDX-FileCopyrightText: 2025 LNRS
+
+SPDX-License-Identifier: AGPL-3.0-or-later
+SPDX-License-Identifier: EUPL-1.2
+-->
+
+<swiss-sanctions-list list-type="whole-list" date="2024-07-30">
+ <target ssid="5816">
+ <sanctions-set-id>4387</sanctions-set-id>
+ <entity>
+ <identity ssid="5818" main="true">
+ <name ssid="5819" name-type="primary-name" quality="good" lang="eng">
+ <name-part order="1" name-part-type="whole-name">
+ <value>LLC Delovaya Set</value>
+ <spelling-variant lang="BEL" script="CYRL" spelling-variant-type="not-defined">ООО Деловая сеть</spelling-variant>
+ </name-part>
+ </name>
+ </identity>
+ <justification ssid="23591">Entity controlled by Vladimir Peftiev.</justification>
+ <relation ssid="6010" target-id="5300" relation-type="related-to"/>
+ </entity>
+ </target>
+</swiss-sanctions-list>
diff --git a/test/data/target_5842.xml b/test/data/target_5842.xml
@@ -0,0 +1,24 @@
+<!--
+SPDX-FileCopyrightText: 2025 LNRS
+
+SPDX-License-Identifier: AGPL-3.0-or-later
+SPDX-License-Identifier: EUPL-1.2
+-->
+
+<swiss-sanctions-list list-type="whole-list" date="2024-07-30">
+ <target ssid="5840">
+ <sanctions-set-id>4387</sanctions-set-id>
+ <entity>
+ <identity ssid="5842" main="true">
+ <name ssid="5843" name-type="primary-name" quality="good" lang="eng">
+ <name-part order="1" name-part-type="whole-name">
+ <value>Spetspriborservice</value>
+ <spelling-variant lang="BEL" script="CYRL" spelling-variant-type="not-defined">Спецприборсервис</spelling-variant>
+ </name-part>
+ </name>
+ </identity>
+ <justification ssid="31513">This company is part of the Beltech Holding.</justification>
+ <other-information ssid="33240">Financial sanctions according to article 1 do not apply until 15 March 2016.</other-information>
+ </entity>
+ </target>
+</swiss-sanctions-list>
diff --git a/test/data/target_5932.xml b/test/data/target_5932.xml
@@ -0,0 +1,23 @@
+<!--
+SPDX-FileCopyrightText: 2025 LNRS
+
+SPDX-License-Identifier: AGPL-3.0-or-later
+SPDX-License-Identifier: EUPL-1.2
+-->
+
+<swiss-sanctions-list list-type="whole-list" date="2024-07-30">
+ <target ssid="5930">
+ <sanctions-set-id>4387</sanctions-set-id>
+ <entity>
+ <identity ssid="5932" main="true">
+ <name ssid="5933" name-type="primary-name" quality="good" lang="eng">
+ <name-part order="1" name-part-type="whole-name">
+ <value>JLLC UnivestStroyInvest</value>
+ <spelling-variant lang="BEL" script="CYRL" spelling-variant-type="not-defined">СООО Юнивест-СтройИнвест</spelling-variant>
+ </name-part>
+ </name>
+ </identity>
+ <justification ssid="5931">Subsidiary of Univest-M.</justification>
+ </entity>
+ </target>
+</swiss-sanctions-list>