diff --git a/bin/test.php b/bin/test.php
index 8880791b1c313e6879e39345c7f32402475a0ceb..cf32aff522b2369015f7b4d50e3a40ccb8406181 100644
--- a/bin/test.php
+++ b/bin/test.php
@@ -1,6 +1,6 @@
 #!/usr/bin/env php
 <?php
-// TODO mivanci remove this file
+// TODO mivanci remove this file before release
 declare(strict_types=1);
 
 use SimpleSAML\Module\accounting\Entities\Authentication\Event;
@@ -34,7 +34,7 @@ echo $newLine;
 
 $spinnerChars = ['|', '/', '-', '\\'];
 
-/**
+/**/
 echo 'Starting simulating MySQL: ';
 $mysqlStartTime = new DateTime();
 echo $mysqlStartTime->format(DateTime::ATOM);
@@ -87,7 +87,7 @@ for ($i = 1; $i <= $numberOfItems; $i++) {
 echo $newLine;
 echo $newLine;
 
-*/
+
 echo 'Starting simulating Redis: ';
 $redisStartTime = new DateTime();
 echo $redisStartTime->format(DateTime::ATOM);
diff --git a/config-templates/module_accounting.php b/config-templates/module_accounting.php
index 62e9a4ec0c68a629b1e885e0e4c96a506073afba..87aa3f61eed001ccb648e88583581dc62468095c 100644
--- a/config-templates/module_accounting.php
+++ b/config-templates/module_accounting.php
@@ -40,14 +40,6 @@ $config = [
          */
         //ModuleConfiguration\AccountingProcessingType::VALUE_ASYNCHRONOUS,
 
-    /**
-     * Cron tags.
-     *
-     * Job runner tag designates the cron tag to use when running accounting jobs. Make sure to add this tag to
-     * the cron module configuration in case of the 'asynchronous' accounting processing type.
-     */
-    ModuleConfiguration::OPTION_CRON_TAG_FOR_JOB_RUNNER => 'accounting_job_runner',
-
     /**
      * Jobs store class. In case of the 'asynchronous' accounting processing type, this determines which class
      * will be used to store jobs. The class must implement Stores\Interfaces\JobsStoreInterface.
@@ -79,14 +71,12 @@ $config = [
          */
         Trackers\Authentication\DoctrineDbal\Versioned\Tracker::class,
 
-
     /**
      * Additional trackers to run besides default data tracker. These trackers will typically only process and
      * persist authentication data to proper data store, and won't be used to display data in SSP UI.
      * These tracker classes must implement Trackers\Interfaces\AuthenticationDataTrackerInterface.
      */
     ModuleConfiguration::OPTION_ADDITIONAL_TRACKERS => [
-        // TODO mivanci at least one more tracker and its connection
         // tracker-class
     ],
 
@@ -164,17 +154,13 @@ $config = [
 
     /**
      * Job runner fine-grained configuration options.
-     */
-
-    /**
-     * Maximum execution time for the job runner.
      *
-     * You can use this option to limit job runner activity by combining when the job runner will run (using
-     * cron configuration) and how long the job runner will be active (execution time). This can be null,
-     * meaning it will run indefinitely, or can be set as a duration for DateInterval, examples being
-     * below. Note that when the job runner is run using Cron user interface in SimpleSAMLphp, the
-     * duration will be taken from the 'max_execution_time' ini setting, and will override this
-     * setting if ini setting is shorter.
+     * Maximum execution time for the job runner. You can use this option to limit job runner activity by combining
+     * when the job runner will run (using cron configuration) and how long the job runner will be active
+     * (execution time). This can be null, meaning it will run indefinitely, or can be set as a duration
+     * for DateInterval, examples being below. Note that when the job runner is run using Cron user
+     * interface in SimpleSAMLphp, the duration will be taken from the 'max_execution_time' ini
+     * setting, and will override this setting if ini setting is shorter.
      * @see https://www.php.net/manual/en/dateinterval.construct.php
      */
     ModuleConfiguration::OPTION_JOB_RUNNER_MAXIMUM_EXECUTION_TIME => null,
@@ -189,4 +175,32 @@ $config = [
      * backend store. If the value is null, there will be no pause.
      */
     ModuleConfiguration::OPTION_JOB_RUNNER_SHOULD_PAUSE_AFTER_NUMBER_OF_JOBS_PROCESSED => 10,
+
+    /**
+     * Tracker data retention policy.
+     *
+     * Determines how long the tracked data will be stored. If null, data will be stored indefinitely. Otherwise, it
+     * can be set as a duration for DateInterval, examples being below. For this to work, a cron tag must also
+     * be configured.
+     */
+    ModuleConfiguration::OPTION_TRACKER_DATA_RETENTION_POLICY => null,
+    //ModuleConfiguration::OPTION_TRACKER_DATA_RETENTION_POLICY => 'P30D', // 30 days
+    //ModuleConfiguration::OPTION_TRACKER_DATA_RETENTION_POLICY => 'P6M', // 6 months
+    //ModuleConfiguration::OPTION_TRACKER_DATA_RETENTION_POLICY => 'P1Y', // 1 year
+
+
+    /**
+     * Cron tags.
+     *
+     * Job runner tag designates the cron tag to use when running accounting jobs. Make sure to add this tag to
+     * the cron module configuration in case of the 'asynchronous' accounting processing type.
+     */
+    ModuleConfiguration::OPTION_CRON_TAG_FOR_JOB_RUNNER => 'accounting_job_runner',
+
+    /**
+     * Tracker data retention policy tag designates the cron tag to use for enforcing data retention policy. Make sure
+     * to add this tag to the cron module configuration if data retention policy is different to null.
+     */
+    ModuleConfiguration::OPTION_CRON_TAG_FOR_TRACKER_DATA_RETENTION_POLICY =>
+        'accounting_tracker_data_retention_policy',
 ];
diff --git a/hooks/hook_adminmenu.php b/hooks/hook_adminmenu.php
index edfee988240a4634c1325c7456fc63a684059284..cb7b83be42f8f3b54cd341bbbe3fd8618cd29827 100644
--- a/hooks/hook_adminmenu.php
+++ b/hooks/hook_adminmenu.php
@@ -19,6 +19,10 @@ function accounting_hook_adminmenu(\SimpleSAML\XHTML\Template &$template): void
         ],
     ];
 
+    if (!isset($template->data[$menuKey]) || !is_array($template->data[$menuKey])) {
+        return;
+    }
+
     // Use array_splice to put our entry before the "Log out" entry.
     array_splice($template->data[$menuKey], -1, 0, $profilePageEntry);
 
diff --git a/hooks/hook_configpage.php b/hooks/hook_configpage.php
index 9b103adc7736fd19a54545dbf798cc335981f97c..daff38442d0f48a95e6e3e0ae2f5ae3c97ad8f96 100644
--- a/hooks/hook_configpage.php
+++ b/hooks/hook_configpage.php
@@ -11,7 +11,13 @@ function accounting_hook_configpage(Template &$template): void
 {
     $moduleRoutesHelper = new ModuleRoutesHelper();
 
-    $template->data['links'][] = [
+    $dataLinksKey = 'links';
+
+    if (!isset($template->data[$dataLinksKey]) || !is_array($template->data[$dataLinksKey])) {
+        return;
+    }
+
+    $template->data[$dataLinksKey][] = [
         'href' => $moduleRoutesHelper->getUrl(ModuleRoutesHelper::PATH_ADMIN_CONFIGURATION_STATUS),
         'text' => Translate::noop('Profile Page configuration status'),
     ];
diff --git a/hooks/hook_cron.php b/hooks/hook_cron.php
index fbc57163510c4c1ba486893958e20c4bcf76efe0..ee6c7c2203a19a3cc82f90b1b93c357109494b4a 100644
--- a/hooks/hook_cron.php
+++ b/hooks/hook_cron.php
@@ -2,20 +2,33 @@
 
 declare(strict_types=1);
 
+use Psr\Log\LoggerInterface;
+use SimpleSAML\Configuration;
+use SimpleSAML\Module\accounting\Services\HelpersManager;
 use SimpleSAML\Module\accounting\Services\JobRunner;
 use SimpleSAML\Module\accounting\ModuleConfiguration;
+use SimpleSAML\Module\accounting\Services\Logger;
+use SimpleSAML\Module\accounting\Trackers\Builders\AuthenticationDataTrackerBuilder;
 
 function accounting_hook_cron(array &$cronInfo): void
 {
     $moduleConfiguration = new ModuleConfiguration();
+    $logger = new Logger();
 
+    /** @var ?string $currentCronTag */
     $currentCronTag = $cronInfo['tag'] ?? null;
 
-    $cronTagForJobRunner = $moduleConfiguration->getCronTagForJobRunner();
+    if (!isset($cronInfo['summary']) || !is_array($cronInfo['summary'])) {
+        $cronInfo['summary'] = [];
+    }
 
+    /**
+     * Job runner handling.
+     */
+    $cronTagForJobRunner = $moduleConfiguration->getCronTagForJobRunner();
     try {
         if ($currentCronTag === $cronTagForJobRunner) {
-            $state = (new JobRunner($moduleConfiguration, \SimpleSAML\Configuration::getConfig()))->run();
+            $state = (new JobRunner($moduleConfiguration, Configuration::getConfig()))->run();
             foreach ($state->getStatusMessages() as $statusMessage) {
                 $cronInfo['summary'][] = $statusMessage;
             }
@@ -31,4 +44,48 @@ function accounting_hook_cron(array &$cronInfo): void
         $message = 'Job runner error: ' . $exception->getMessage();
         $cronInfo['summary'][] = $message;
     }
+
+    if (!isset($cronInfo['summary']) || !is_array($cronInfo['summary'])) {
+        $cronInfo['summary'] = [];
+    }
+
+    /**
+     * Tracker data retention policy handling.
+     */
+    $cronTagForTrackerDataRetentionPolicy = $moduleConfiguration->getCronTagForTrackerDataRetentionPolicy();
+    try {
+        if (
+            $currentCronTag === $cronTagForTrackerDataRetentionPolicy &&
+            ($retentionPolicy = $moduleConfiguration->getTrackerDataRetentionPolicy()) !== null
+        ) {
+            $helpersManager = new HelpersManager();
+            $message = sprintf('Handling data retention policy.');
+            $logger->info($message);
+            $cronInfo['summary'][] = $message;
+            handleDataRetentionPolicy($moduleConfiguration, $logger, $helpersManager, $retentionPolicy);
+        }
+    } catch (Throwable $exception) {
+        $message = 'Error enforcing tracker data retention policy: ' . $exception->getMessage();
+        $cronInfo['summary'][] = $message;
+    }
+}
+
+function handleDataRetentionPolicy(
+    ModuleConfiguration $moduleConfiguration,
+    LoggerInterface $logger,
+    HelpersManager $helpersManager,
+    DateInterval $retentionPolicy
+): void {
+    // Handle default data tracker and provider
+    (new AuthenticationDataTrackerBuilder($moduleConfiguration, $logger, $helpersManager))
+        ->build($moduleConfiguration->getDefaultDataTrackerAndProviderClass())
+        ->enforceDataRetentionPolicy($retentionPolicy);
+
+    $additionalTrackers = $moduleConfiguration->getAdditionalTrackers();
+
+    foreach ($additionalTrackers as $tracker) {
+        (new AuthenticationDataTrackerBuilder($moduleConfiguration, $logger, $helpersManager))
+            ->build($tracker)
+            ->enforceDataRetentionPolicy($retentionPolicy);
+    }
 }
\ No newline at end of file
diff --git a/psalm.xml b/psalm.xml
index 87c90075a861695c2d56c923737061571b81524e..ccad21aa75b84382afc60f916d787ce5105425c1 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -11,6 +11,7 @@
         <directory name="config-templates" />
         <directory name="tests" />
         <directory name="www" />
+        <directory name="hooks" />
 
         <ignoreFiles>
             <directory name="vendor" />
diff --git a/routing/routes/routes.yml b/routing/routes/routes.yml
index c6bc1a186a23913a851a3aa3e93af975bc7e2ea0..138cde0773523be99175e3bc371a80168237d921 100644
--- a/routing/routes/routes.yml
+++ b/routing/routes/routes.yml
@@ -1,12 +1,8 @@
 
-accounting-setup:
-    path:       /setup
-    defaults:   { _controller: 'SimpleSAML\Module\accounting\Http\Controllers\Test::setup' }
-
-# TODO mivanci delete
-accounting-job-runner:
-    path:       /job-runner
-    defaults:   { _controller: 'SimpleSAML\Module\accounting\Http\Controllers\Test::jobRunner' }
+# TODO mivanci delete test route
+accounting-test:
+    path:       /test
+    defaults:   { _controller: 'SimpleSAML\Module\accounting\Http\Controllers\Test::test' }
 
 accounting-admin-configuration-status:
     path:       /admin/configuration/status
diff --git a/src/Auth/Process/Accounting.php b/src/Auth/Process/Accounting.php
index a2488982fae8cc4911737a18f8f73005a6547344..817fa9e6636cb0e87eb86b9b1e8291f4c103dfac 100644
--- a/src/Auth/Process/Accounting.php
+++ b/src/Auth/Process/Accounting.php
@@ -72,7 +72,6 @@ class Accounting extends ProcessingFilter
                 $this->moduleConfiguration->getAdditionalTrackers()
             );
 
-            /** @var string $tracker */
             foreach ($configuredTrackers as $tracker) {
                 ($this->authenticationDataTrackerBuilder->build($tracker))->process($authenticationEvent);
             }
diff --git a/src/Helpers/RandomHelper.php b/src/Helpers/RandomHelper.php
index 30256837765db4ebe4806e951d31ae66974af799..3a74a7fe318a6705c8835bc10a2ae02c72e90d63 100644
--- a/src/Helpers/RandomHelper.php
+++ b/src/Helpers/RandomHelper.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace SimpleSAML\Module\accounting\Helpers;
 
 class RandomHelper
@@ -8,8 +10,10 @@ class RandomHelper
     {
         try {
             return random_int($minimum, $maximum);
+            // @codeCoverageIgnoreStart
         } catch (\Throwable $exception) {
             return mt_rand($minimum, $maximum);
+            // @codeCoverageIgnoreEnd
         }
     }
 }
diff --git a/src/Http/Controllers/Admin/Configuration.php b/src/Http/Controllers/Admin/Configuration.php
index 3f585d1e1adb006f08f0801d243eca2c981dae07..909518ce4e4dbdf56fc72f881c67870c7546c668 100644
--- a/src/Http/Controllers/Admin/Configuration.php
+++ b/src/Http/Controllers/Admin/Configuration.php
@@ -55,6 +55,7 @@ class Configuration
         $configurationValidationErrors = null;
         $jobsStore = null;
         $defaultDataTrackerAndProvider = null;
+        $additionalTrackers = [];
         $setupNeeded = false;
         $runSetup = $request->query->has('runSetup');
 
@@ -87,7 +88,21 @@ class Configuration
                     }
                 }
             }
-            // TODO mivanci setup for additional trackers from configuration
+
+            foreach ($moduleConfiguration->getAdditionalTrackers() as $trackerClass) {
+                $additionalTrackerInstance =
+                    (new AuthenticationDataTrackerBuilder($moduleConfiguration, $this->logger, $this->helpersManager))
+                    ->build($trackerClass);
+
+                if ($additionalTrackerInstance->needsSetup()) {
+                    if ($runSetup) {
+                        $additionalTrackerInstance->runSetup();
+                    } else {
+                        $setupNeeded = true;
+                    }
+                }
+                $additionalTrackers[$trackerClass] = $additionalTrackerInstance;
+            }
         } catch (Throwable $exception) {
             $configurationValidationErrors = $exception->getMessage();
         }
@@ -97,6 +112,7 @@ class Configuration
             'configurationValidationErrors' => $configurationValidationErrors,
             'jobsStore' => $jobsStore,
             'defaultDataTrackerAndProvider' => $defaultDataTrackerAndProvider,
+            'additionalTrackers' => $additionalTrackers,
             'setupNeeded' => $setupNeeded,
             'profilePageUri' => $this->helpersManager->getModuleRoutesHelper()
                 ->getUrl(ModuleRoutesHelper::PATH_USER_PERSONAL_DATA),
diff --git a/src/Http/Controllers/Test.php b/src/Http/Controllers/Test.php
index 697a50ad3c06ba935a4637c47148fb33f21bd8a3..e7b4446d6942869d0d30d92e60655bef7c047ae5 100644
--- a/src/Http/Controllers/Test.php
+++ b/src/Http/Controllers/Test.php
@@ -12,6 +12,7 @@ use SimpleSAML\Locale\Translate;
 use SimpleSAML\Module\accounting\Entities\Authentication\Event;
 use SimpleSAML\Module\accounting\Entities\Authentication\State;
 use SimpleSAML\Module\accounting\ModuleConfiguration;
+use SimpleSAML\Module\accounting\Services\HelpersManager;
 use SimpleSAML\Module\accounting\Stores\Builders\JobsStoreBuilder;
 use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store;
 use SimpleSAML\Module\accounting\Trackers\Authentication\DoctrineDbal\Versioned\Tracker;
@@ -30,6 +31,7 @@ class Test
     protected Session $session;
     protected ModuleConfiguration $moduleConfiguration;
     protected LoggerInterface $logger;
+    protected HelpersManager $helpersManager;
 
     /**
      * @param SspConfiguration $sspConfiguration
@@ -41,12 +43,14 @@ class Test
         SspConfiguration $sspConfiguration,
         Session $session,
         ModuleConfiguration $moduleConfiguration,
-        LoggerInterface $logger
+        LoggerInterface $logger,
+        HelpersManager $helpersManager
     ) {
         $this->sspConfiguration = $sspConfiguration;
         $this->session = $session;
         $this->moduleConfiguration = $moduleConfiguration;
         $this->logger = $logger;
+        $this->helpersManager = $helpersManager;
     }
 
     /**
@@ -54,90 +58,18 @@ class Test
      * @return Template
      * @throws Exception
      */
-    public function setup(Request $request): Template
+    public function test(Request $request): Template
     {
         $template = new Template($this->sspConfiguration, 'accounting:test.twig');
 
-        $jobsStore = (new JobsStoreBuilder($this->moduleConfiguration, $this->logger))
-            ->build($this->moduleConfiguration->getJobsStoreClass());
+        $retentionPolicy = new \DateInterval('P4D');
 
-        // TODO refactor to tracker builder
-        $dataStoreConnectionKey = $this->moduleConfiguration->getClassConnectionKey(Tracker::class);
-        $dataStore = Store::build($this->moduleConfiguration, $this->logger, $dataStoreConnectionKey);
+        (new AuthenticationDataTrackerBuilder($this->moduleConfiguration, $this->logger, $this->helpersManager))
+            ->build($this->moduleConfiguration->getDefaultDataTrackerAndProviderClass())
+            ->enforceDataRetentionPolicy($retentionPolicy);
 
-        $jobsStoreNeedsSetup = $jobsStore->needsSetup();
-        $jobsStoreSetupRan = false;
+        die('end');
 
-        $dataStoreNeedsSetup = $dataStore->needsSetup();
-        $dataStoreSetupRan = false;
-
-
-        if ($jobsStoreNeedsSetup && $request->query->has('setup')) {
-            $this->logger->info('Jobs Store setup ran.');
-            $jobsStore->runSetup();
-            $jobsStoreSetupRan = true;
-        }
-
-        if ($dataStoreNeedsSetup && $request->query->has('setup')) {
-            $this->logger->info('Data Store setup ran.');
-            $dataStore->runSetup();
-            $dataStoreSetupRan = true;
-        }
-
-        $sampleStateArray = ['Responder'=>[0=>'\\SimpleSAML\\Module\\saml\\IdP\\SAML2',1=>'sendResponse',],'\\SimpleSAML\\Auth\\State.exceptionFunc'=>[0=>'\\SimpleSAML\\Module\\saml\\IdP\\SAML2',1=>'handleAuthError',],'\\SimpleSAML\\Auth\\State.restartURL'=>'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/saml2/idp/SSOService.php?spentityid=https%3A%2F%2Fpc-example.org.hr%3A9074%2Fsimplesamlphp%2Fsimplesamlphp-2-beta-git%2Fmodule.php%2Fsaml%2Fsp%2Fmetadata.php%2Fdefault-sp&RelayState=https%3A%2F%2Flocalhost.someone.from.hr%3A9074%2Fsimplesamlphp%2Fsimplesamlphp-2-beta-git%2Fmodule.php%2Fadmin%2Ftest%2Fdefault-sp&cookieTime=1660912195','SPMetadata'=>['SingleLogoutService'=>[0=>['Binding'=>'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect','Location'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/singleLogoutService/default-sp',],],'AssertionConsumerService'=>[0=>['Binding'=>'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST','Location'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/assertionConsumerService/default-sp','index'=>0,],1=>['Binding'=>'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact','Location'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/assertionConsumerService/default-sp','index'=>1,],],'contacts'=>[0=>['emailAddress'=>'example@org.hr','givenName'=>'MarkoIvančić','contactType'=>'technical',],],'entityid'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp','metadata-index'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp','metadata-set'=>'saml20-sp-remote',],'saml:RelayState'=>'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/admin/test/default-sp','saml:RequestId'=>null,'saml:IDPList'=>[],'saml:ProxyCount'=>null,'saml:RequesterID'=>null,'ForceAuthn'=>false,'isPassive'=>false,'saml:ConsumerURL'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/assertionConsumerService/default-sp','saml:Binding'=>'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST','saml:NameIDFormat'=>null,'saml:AllowCreate'=>true,'saml:Extensions'=>null,'saml:AuthnRequestReceivedAt'=>1660912195.505402,'saml:RequestedAuthnContext'=>null,'core:IdP'=>'saml2:https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/saml2/idp/metadata.php','core:SP'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp','IdPMetadata'=>['host'=>'localhost.someone.from.hr','privatekey'=>'key.pem','certificate'=>'cert.pem','auth'=>'example-userpass','attributes.NameFormat'=>'urn:oasis:names:tc:SAML:2.0:attrname-format:uri','authproc'=>[100=>['class'=>'core:AttributeMap',0=>'name2oid',],],'entityid'=>'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/saml2/idp/metadata.php','metadata-index'=>'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/saml2/idp/metadata.php','metadata-set'=>'saml20-idp-hosted',],'ReturnCallback'=>[0=>'\\SimpleSAML\\IdP',1=>'postAuth',],'Attributes'=>['hrEduPersonUniqueID'=>[0=>'testuser@primjer2.hr',],'urn:oid:0.9.2342.19200300.100.1.1'=>[0=>'testuser',],'urn:oid:2.5.4.3'=>[0=>'TestNameTestSurname',],'urn:oid:2.5.4.4'=>[0=>'TestSurname',1=>'TestSurname2',],'urn:oid:2.5.4.42'=>[0=>'TestName',],'urn:oid:0.9.2342.19200300.100.1.3'=>[0=>'testusermail@primjer.hr',1=>'testusermail2@primjer.hr',],'urn:oid:2.5.4.20'=>[0=>'123',],'hrEduPersonExtensionNumber'=>[0=>'123',],'urn:oid:0.9.2342.19200300.100.1.41'=>[0=>'123',],'urn:oid:2.5.4.23'=>[0=>'123',],'hrEduPersonUniqueNumber'=>[0=>'LOCAL_NO:100',1=>'OIB:00861590360',],'hrEduPersonOIB'=>[0=>'00861590360',],'hrEduPersonDateOfBirth'=>[0=>'20200101',],'hrEduPersonGender'=>[0=>'9',],'urn:oid:0.9.2342.19200300.100.1.60'=>[0=>'987654321',],'urn:oid:2.5.4.36'=>[0=>'cert-value',],'urn:oid:1.3.6.1.4.1.250.1.57'=>[0=>'http://www.org.hr/~ivekHomePage',],'hrEduPersonProfessionalStatus'=>[0=>'VSS',],'hrEduPersonAcademicStatus'=>[0=>'redovitiprofesor',],'hrEduPersonScienceArea'=>[0=>'sociologija',],'hrEduPersonAffiliation'=>[0=>'djelatnik',1=>'student',],'hrEduPersonPrimaryAffiliation'=>[0=>'djelatnik',],'hrEduPersonStudentCategory'=>[0=>'redovitistudent:diplomskisveučilišnistudij',],'hrEduPersonExpireDate'=>[0=>'20211231',],'hrEduPersonTitle'=>[0=>'rektor',],'hrEduPersonRole'=>[0=>'ICTkoordinator',1=>'ISVUkoordinator',],'hrEduPersonStaffCategory'=>[0=>'nastavnoosoblje',1=>'istraživači',],'hrEduPersonGroupMember'=>[0=>'grupa1',1=>'grupa2',],'urn:oid:2.5.4.10'=>[0=>'Testnaustanova',],'hrEduPersonHomeOrg'=>[0=>'primjer.hr',],'urn:oid:2.5.4.11'=>[0=>'Testnaorgjedinica',],'urn:oid:0.9.2342.19200300.100.1.6'=>[0=>'123',],'urn:oid:2.5.4.16'=>[0=>'Testnaustanova,Baštijanova52A,HR-10000Zagreb',],'urn:oid:2.5.4.7'=>[0=>'Zagreb',],'urn:oid:2.5.4.17'=>[0=>'HR-10000',],'urn:oid:2.5.4.9'=>[0=>'Baštijanova52A',],'urn:oid:0.9.2342.19200300.100.1.39'=>[0=>'Testnaadresa52A,HR-10000,Zagreb',],'urn:oid:0.9.2342.19200300.100.1.20'=>[0=>'123',],'hrEduPersonCommURI'=>[0=>'123',],'hrEduPersonPrivacy'=>[0=>'street',1=>'postalCode',],'hrEduPersonPersistentID'=>[0=>'da4294fb4e5746d57ab6ad88d2daf275',],'urn:oid:2.16.840.1.113730.3.1.241'=>[0=>'testname123',],'urn:oid:1.3.6.1.4.1.25178.1.2.12'=>[0=>'skype:pepe.perez',],'hrEduPersonCardNum'=>[0=>'123',],'updatedAt'=>[0=>'123456789',],'urn:oid:1.3.6.1.4.1.5923.1.1.1.7'=>[0=>'urn:example:oidc:manage:client',],],'Authority'=>'example-userpass','AuthnInstant'=>1660911943,'Expire'=>1660940743,'ReturnCall'=>[0=>'\\SimpleSAML\\IdP',1=>'postAuthProc',],'Destination'=>['SingleLogoutService'=>[0=>['Binding'=>'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect','Location'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/singleLogoutService/default-sp',],],'AssertionConsumerService'=>[0=>['Binding'=>'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST','Location'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/assertionConsumerService/default-sp','index'=>0,],1=>['Binding'=>'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact','Location'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/assertionConsumerService/default-sp','index'=>1,],],'contacts'=>[0=>['emailAddress'=>'example@org.hr','givenName'=>'MarkoIvančić','contactType'=>'technical',],],'entityid'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp','metadata-index'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp','metadata-set'=>'saml20-sp-remote',],'Source'=>['host'=>'localhost.someone.from.hr','privatekey'=>'key.pem','certificate'=>'cert.pem','auth'=>'example-userpass','attributes.NameFormat'=>'urn:oasis:names:tc:SAML:2.0:attrname-format:uri','authproc'=>[100=>['class'=>'core:AttributeMap',0=>'name2oid',],],'entityid'=>'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/saml2/idp/metadata.php','metadata-index'=>'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/saml2/idp/metadata.php','metadata-set'=>'saml20-idp-hosted',],'\\SimpleSAML\\Auth\\ProcessingChain.filters'=>[],];
-        $state = new State($sampleStateArray);
-
-        $authenticationEvent = new Event($state);
-
-        //$dataStore->persist($authenticationEvent);
-
-        $data = [
-            'jobs_store' => $this->moduleConfiguration->getJobsStoreClass(),
-            'jobs_store_needs_setup' => $jobsStoreNeedsSetup ? 'yes' : 'no',
-            'jobs_store_setup_ran' => $jobsStoreSetupRan ? 'yes' : 'no',
-
-            'data_store' => get_class($dataStore),
-            'data_store_needs_setup' => $dataStoreNeedsSetup ? 'yes' : 'no',
-            'data_store_setup_ran' => $dataStoreSetupRan ? 'yes' : 'no',
-        ];
-
-        die(var_dump($data));
-
-        $template->data = $data;
         return $template;
     }
-
-    public function jobRunner():void
-    {
-        $jobsStore = (new JobsStoreBuilder($this->moduleConfiguration, $this->logger))
-            ->build($this->moduleConfiguration->getJobsStoreClass());
-
-
-        $trackerBuilder = new AuthenticationDataTrackerBuilder($this->moduleConfiguration, $this->logger);
-
-        $configuredTrackers = array_merge(
-            [$this->moduleConfiguration->getDefaultDataTrackerAndProviderClass()],
-            $this->moduleConfiguration->getAdditionalTrackers()
-        );
-
-        $start = new \DateTimeImmutable();
-        $data = [
-            'jobs_processed' => 0,
-            'start_time' => $start->format('Y-m-d H:i:s.u'),
-        ];
-
-        /** @var Event\Job $job */
-        while (($job = $jobsStore->dequeue(Event\Job::class)) !== null) {
-            foreach ($configuredTrackers as $tracker) {
-                ($trackerBuilder->build($tracker))->process($job->getPayload());
-                $data['jobs_processed']++;
-            }
-        }
-
-        $end = new \DateTimeImmutable();
-        $data['end_time'] = $end->format('Y-m-d H:i:s.u');
-        $data['duration'] = $end->diff($start)->format('%s seconds, %f microseconds');
-
-        die(var_dump($data));
-    }
 }
diff --git a/src/Http/Controllers/User/Profile.php b/src/Http/Controllers/User/Profile.php
index 9f4d5ecf27a81218edb970f3330059b222a0d20c..52f392c33d632b26e319a53a5be2dd4e41ddf3cc 100644
--- a/src/Http/Controllers/User/Profile.php
+++ b/src/Http/Controllers/User/Profile.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace SimpleSAML\Module\accounting\Http\Controllers\User;
 
 use Psr\Log\LoggerInterface;
@@ -18,9 +20,6 @@ use SimpleSAML\XHTML\Template;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 
-/**
- * @psalm-suppress all TODO mivanci remove this psalm suppress after testing
- */
 class Profile
 {
     protected ModuleConfiguration $moduleConfiguration;
@@ -58,11 +57,11 @@ class Profile
         $this->defaultAuthenticationSource = $moduleConfiguration->getDefaultAuthenticationSource();
         $this->authSimple = $authSimple ?? new Simple($this->defaultAuthenticationSource, $sspConfiguration, $session);
 
-        $this->authenticationDataProviderBuilder = $authenticationDataProviderBuilder ??
-            new AuthenticationDataProviderBuilder($this->moduleConfiguration, $this->logger);
-
         $this->helpersManager = $helpersManager ?? new HelpersManager();
 
+        $this->authenticationDataProviderBuilder = $authenticationDataProviderBuilder ??
+            new AuthenticationDataProviderBuilder($this->moduleConfiguration, $this->logger, $this->helpersManager);
+
         // Make sure the end user is authenticated.
         $this->authSimple->requireAuth();
     }
@@ -80,7 +79,7 @@ class Profile
         foreach ($this->authSimple->getAttributes() as $name => $value) {
             // Convert attribute names to user-friendly names.
             if (array_key_exists($name, $toNameAttributeMap)) {
-                $name = $toNameAttributeMap[$name];
+                $name = (string)$toNameAttributeMap[$name];
             }
             $normalizedAttributes[$name] = implode('; ', $value);
         }
@@ -114,7 +113,6 @@ class Profile
 
         $page = ($page = (int)$request->query->get('page', 1)) > 0 ? $page : 1;
 
-        // TODO mivanci make maxResults configurable
         $maxResults = 10;
         $firstResult = ($page - 1) * $maxResults;
 
@@ -177,9 +175,9 @@ class Profile
     /** TODO mivanci remove after debugging */
     protected function removeDebugDisplayLimits(): void
     {
-        ini_set('xdebug.var_display_max_depth', -1);
-        ini_set('xdebug.var_display_max_children', -1);
-        ini_set('xdebug.var_display_max_data', -1);
+        ini_set('xdebug.var_display_max_depth', '-1');
+        ini_set('xdebug.var_display_max_children', '-1');
+        ini_set('xdebug.var_display_max_data', '-1');
     }
 
     protected function resolveTemplate(string $template): Template
diff --git a/src/ModuleConfiguration.php b/src/ModuleConfiguration.php
index 1827a1401191fc90bc710ae3e0262940b454a5ab..fe83f63151d45129b09dde07630cb4e0df2ca903 100644
--- a/src/ModuleConfiguration.php
+++ b/src/ModuleConfiguration.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
 
 namespace SimpleSAML\Module\accounting;
 
+use DateInterval;
 use Exception;
 use SimpleSAML\Configuration;
 use SimpleSAML\Module\accounting\Exceptions\InvalidConfigurationException;
@@ -35,6 +36,8 @@ class ModuleConfiguration
     public const OPTION_JOB_RUNNER_MAXIMUM_EXECUTION_TIME = 'job_runner_maximum_execution_time';
     public const OPTION_JOB_RUNNER_SHOULD_PAUSE_AFTER_NUMBER_OF_JOBS_PROCESSED =
         'job_runner_should_pause_after_number_of_jobs_processed';
+    public const OPTION_TRACKER_DATA_RETENTION_POLICY = 'tracker_data_retention_policy';
+    public const OPTION_CRON_TAG_FOR_TRACKER_DATA_RETENTION_POLICY = 'cron_tag_for_tracker_data_retention_policy';
 
     /**
      * Contains configuration from module configuration file.
@@ -80,7 +83,7 @@ class ModuleConfiguration
         return $this->getConfiguration()->getString(self::OPTION_JOBS_STORE);
     }
 
-    public function getJobRunnerMaximumExecutionTime(): ?\DateInterval
+    public function getJobRunnerMaximumExecutionTime(): ?DateInterval
     {
         $value = $this->get(self::OPTION_JOB_RUNNER_MAXIMUM_EXECUTION_TIME);
 
@@ -95,7 +98,7 @@ class ModuleConfiguration
         }
 
         try {
-            return new \DateInterval($value);
+            return new DateInterval($value);
         } catch (Throwable $exception) {
             $message = sprintf('Can not create DateInterval instance using value %s as parameter.', $value);
             throw new InvalidConfigurationException($message);
@@ -139,6 +142,10 @@ class ModuleConfiguration
         return $this->getConfiguration()->getArray(self::OPTION_CONNECTIONS_AND_PARAMETERS);
     }
 
+    /**
+     * @return string[]
+     * @psalm-suppress MixedReturnTypeCoercion We specifically check if valid class string are provided.
+     */
     public function getAdditionalTrackers(): array
     {
         return $this->getConfiguration()->getArray(self::OPTION_ADDITIONAL_TRACKERS);
@@ -298,6 +305,13 @@ class ModuleConfiguration
             $errors[] = $exception->getMessage();
         }
 
+        try {
+            $this->validateTrackerDataRetentionPolicy();
+        } catch (Throwable $exception) {
+            $errors[] = $exception->getMessage();
+        }
+
+
         if (!empty($errors)) {
             $message = sprintf('Module configuration validation failed with errors: %s', implode(' ', $errors));
             throw new InvalidConfigurationException($message);
@@ -343,9 +357,6 @@ class ModuleConfiguration
         $errors = [];
 
         // Validate additional trackers
-        /**
-         * @var string $trackerClass
-         */
         foreach ($this->getAdditionalTrackers() as $trackerClass) {
             /** @psalm-suppress DocblockTypeContradiction */
             if (!is_string($trackerClass)) {
@@ -444,4 +455,34 @@ class ModuleConfiguration
     {
         $this->getCronTagForJobRunner();
     }
+
+    protected function validateTrackerDataRetentionPolicy(): void
+    {
+        if ($this->getTrackerDataRetentionPolicy() !== null) {
+            $this->getCronTagForTrackerDataRetentionPolicy();
+        }
+    }
+
+    public function getTrackerDataRetentionPolicy(): ?DateInterval
+    {
+        /** @var string|null $value */
+        $value = $this->getConfiguration()
+            ->getOptionalString(self::OPTION_TRACKER_DATA_RETENTION_POLICY, null);
+
+        if (is_null($value)) {
+            return null;
+        }
+
+        try {
+            return new DateInterval($value);
+        } catch (Throwable $exception) {
+            $message = sprintf('Can not create DateInterval instance using value %s as parameter.', $value);
+            throw new InvalidConfigurationException($message);
+        }
+    }
+
+    public function getCronTagForTrackerDataRetentionPolicy(): string
+    {
+        return $this->getConfiguration()->getString(self::OPTION_CRON_TAG_FOR_TRACKER_DATA_RETENTION_POLICY);
+    }
 }
diff --git a/src/Services/JobRunner.php b/src/Services/JobRunner.php
index eb179489a113a185c12d7f411f0e188e2aa76ada..b08d96f17e4903a5b12b4def57108e3544f956b8 100644
--- a/src/Services/JobRunner.php
+++ b/src/Services/JobRunner.php
@@ -493,7 +493,6 @@ class JobRunner
             $this->moduleConfiguration->getAdditionalTrackers()
         );
 
-        /** @var string $trackerClass */
         foreach ($configuredTrackerClasses as $trackerClass) {
             $trackers[$trackerClass] = $this->authenticationDataTrackerBuilder->build($trackerClass);
         }
diff --git a/src/Services/JobRunner/State.php b/src/Services/JobRunner/State.php
index 74eb2e4afd714e5eb4e209213ded6fe9d9a4a8ac..3d1704927002a2464d3b06bc0f73b8da8f8c2246 100644
--- a/src/Services/JobRunner/State.php
+++ b/src/Services/JobRunner/State.php
@@ -15,6 +15,9 @@ class State
     protected ?\DateTimeImmutable $endedAt = null;
     protected int $successfulJobsProcessed = 0;
     protected int $failedJobsProcessed = 0;
+    /**
+     * @var string[]
+     */
     protected array $statusMessages = [];
     protected int $numberOfStatusMessagesToKeep = 10;
     protected bool $isGracefulInterruptInitiated = false;
@@ -161,6 +164,9 @@ class State
         }
     }
 
+    /**
+     * @return string[]
+     */
     public function getStatusMessages(): array
     {
         return $this->statusMessages;
@@ -172,7 +178,7 @@ class State
             return null;
         }
 
-        $message = (string)end($this->statusMessages);
+        $message = end($this->statusMessages);
         reset($this->statusMessages);
 
         return $message;
diff --git a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store.php b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store.php
index 37319f1d486f33880628fd87d662883e4b9303e6..ce3478f1ed114d9a0734807e12326ddc285654c3 100644
--- a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store.php
+++ b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store.php
@@ -506,76 +506,6 @@ class Store extends AbstractStore implements DataStoreInterface
         }
 
         return $connectedServiceProviderBag;
-
-        // TODO mivanci remove after unit tests
-//        $authenticationEventsQueryBuilder = $this->connection->dbal()->createQueryBuilder();
-//        $lastMetadataAndAttributesQueryBuilder = $this->connection->dbal()->createQueryBuilder();
-//
-//        /** @psalm-suppress TooManyArguments */
-//        $authenticationEventsQueryBuilder->select(
-//            'vs.entity_id AS sp_entity_id',
-//            'COUNT(vae.id) AS number_of_authentications',
-//            'MAX(vae.happened_at) AS last_authentication_at',
-//            'MIN(vae.happened_at) AS first_authentication_at',
-//        )->from('vds_authentication_event', 'vae')
-//            ->leftJoin(
-//                'vae',
-//                'vds_idp_sp_user_version',
-//                'visuv',
-//                'vae.idp_sp_user_version_id = visuv.id'
-//            )
-//            ->leftJoin('visuv', 'vds_sp_version', 'vsv', 'visuv.sp_version_id = vsv.id')
-//            ->leftJoin('vsv', 'vds_sp', 'vs', 'vsv.sp_id = vs.id')
-//            ->leftJoin('visuv', 'vds_user_version', 'vuv', 'visuv.user_version_id = vuv.id')
-//            ->leftJoin('vuv', 'vds_user', 'vu', 'vuv.user_id = vu.id')
-//            ->where(
-//                'vu.identifier_hash_sha256 = ' .
-//                $authenticationEventsQueryBuilder->createNamedParameter($userIdentifierHashSha256)
-//            )
-//            ->groupBy('vs.id')
-//            ->orderBy('number_of_authentications', 'DESC');
-//
-//        /** @psalm-suppress TooManyArguments */
-//        $lastMetadataAndAttributesQueryBuilder->select(
-//            'vs.entity_id AS sp_entity_id',
-//            'vsv.metadata AS sp_metadata',
-//            'vuv.attributes AS user_attributes',
-//            //            'vsv.id AS sp_version_id',
-//            //            'vuv.id AS user_version_id',
-//        )->from('vds_authentication_event', 'vae')
-//            ->leftJoin(
-//                'vae',
-//                'vds_idp_sp_user_version',
-//                'visuv',
-//                'vae.idp_sp_user_version_id = visuv.id'
-//            )
-//            ->leftJoin('visuv', 'vds_sp_version', 'vsv', 'visuv.sp_version_id = vsv.id')
-//            ->leftJoin('vsv', 'vds_sp', 'vs', 'vsv.sp_id = vs.id')
-//            ->leftJoin('visuv', 'vds_user_version', 'vuv', 'visuv.user_version_id = vuv.id')
-//            ->leftJoin('vuv', 'vds_user', 'vu', 'vuv.user_id = vu.id')
-//            ->leftJoin('vsv', 'vds_sp_version', 'vsv2', 'vsv.id = vsv2.id AND vsv.id < vsv2.id')
-//            ->leftJoin('vuv', 'vds_user_version', 'vuv2', 'vuv.id = vuv2.id AND vuv.id < vuv2.id')
-//            ->where(
-//                'vu.identifier_hash_sha256 = ' .
-//                $lastMetadataAndAttributesQueryBuilder->createNamedParameter($userIdentifierHashSha256)
-//            )
-//            ->andWhere('vsv2.id IS NULL')
-//            ->andWhere('vuv2.id IS NULL');
-//
-//        try {
-//            $numberOfAuthentications =
-// $authenticationEventsQueryBuilder->executeQuery()->fetchAllAssociativeIndexed();
-//            $lastMetadataAndAttributes =
-//                $lastMetadataAndAttributesQueryBuilder->executeQuery()->fetchAllAssociativeIndexed();
-//
-//            return array_merge_recursive($numberOfAuthentications, $lastMetadataAndAttributes);
-//        } catch (\Throwable $exception) {
-//            $message = sprintf(
-//                'Error executing query to get connected organizations. Error was: %s.',
-//                $exception->getMessage()
-//            );
-//            throw new StoreException($message, (int)$exception->getCode(), $exception);
-//        }
     }
 
 
@@ -584,7 +514,6 @@ class Store extends AbstractStore implements DataStoreInterface
      */
     public function getActivity(string $userIdentifierHashSha256, int $maxResults, int $firstResult): Activity\Bag
     {
-        // TODO mivanci pagination
         $results =  $this->repository->getActivity($userIdentifierHashSha256, $maxResults, $firstResult);
 
         $activityBag = new Activity\Bag();
@@ -618,42 +547,11 @@ class Store extends AbstractStore implements DataStoreInterface
         }
 
         return $activityBag;
+    }
 
-        // TODO mivanci remove
-//        $authenticationEventsQueryBuilder = $this->connection->dbal()->createQueryBuilder();
-//
-//        /** @psalm-suppress TooManyArguments */
-//        $authenticationEventsQueryBuilder->select(
-//            'vae.happened_at',
-//            'vsv.metadata AS sp_metadata',
-//            'vuv.attributes AS user_attributes'
-//        )->from('vds_authentication_event', 'vae')
-//            ->leftJoin(
-//                'vae',
-//                'vds_idp_sp_user_version',
-//                'visuv',
-//                'vae.idp_sp_user_version_id = visuv.id'
-//            )
-//            ->leftJoin('visuv', 'vds_sp_version', 'vsv', 'visuv.sp_version_id = vsv.id')
-//            ->leftJoin('vsv', 'vds_sp', 'vs', 'vsv.sp_id = vs.id')
-//            ->leftJoin('visuv', 'vds_user_version', 'vuv', 'visuv.user_version_id = vuv.id')
-//            ->leftJoin('vuv', 'vds_user', 'vu', 'vuv.user_id = vu.id')
-//            ->where(
-//                'vu.identifier_hash_sha256 = ' .
-//                $authenticationEventsQueryBuilder->createNamedParameter($userIdentifierHashSha256)
-//            )
-//            ->orderBy('vae.id', 'DESC');
-//
-//        try {
-//            $numberOfAuthentications = $authenticationEventsQueryBuilder->executeQuery()->fetchAllAssociative();
-//
-//            return array_merge_recursive($numberOfAuthentications);
-//        } catch (\Throwable $exception) {
-//            $message = sprintf(
-//                'Error executing query to get connected organizations. Error was: %s.',
-//                $exception->getMessage()
-//            );
-//            throw new StoreException($message, (int)$exception->getCode(), $exception);
-//        }
+    public function deleteDataOlderThan(\DateTimeImmutable $dateTime): void
+    {
+        // Only delete authentication events. Versioned data (IdP / SP metadata, user attributes) remain.
+        $this->repository->deleteAuthenticationEventsOlderThan($dateTime);
     }
 }
diff --git a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000700CreateAuthenticationEventTable.php b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000700CreateAuthenticationEventTable.php
index 87938387f3de5f4a645b1b94e6d97928769802bd..be8e304cd55d8db5d95093d454758b2f3503ae3b 100644
--- a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000700CreateAuthenticationEventTable.php
+++ b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000700CreateAuthenticationEventTable.php
@@ -50,6 +50,9 @@ class Version20220801000700CreateAuthenticationEventTable extends AbstractMigrat
                 ['id']
             );
 
+            // Old data can be deleted using happened_at column, so add index for it.
+            $table->addIndex(['happened_at']);
+
             $this->schemaManager->createTable($table);
         } catch (\Throwable $exception) {
             throw $this->prepareGenericMigrationException(
diff --git a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Repository.php b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Repository.php
index 0c198896eea6dec297fd2b4270c15cc955bf2cb4..24c9b34492e51555959e33a3e482736b1ea7bd22 100644
--- a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Repository.php
+++ b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Repository.php
@@ -1066,4 +1066,28 @@ class Repository
             throw new StoreException($message, (int)$exception->getCode(), $exception);
         }
     }
+
+    /**
+     * @throws StoreException
+     */
+    public function deleteAuthenticationEventsOlderThan(\DateTimeImmutable $dateTime): void
+    {
+        try {
+            $queryBuilder = $this->connection->dbal()->createQueryBuilder();
+
+            $queryBuilder->delete($this->tableNameAuthenticationEvent)
+                ->where(
+                    $queryBuilder->expr()->lt(
+                        TableConstants::TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_HAPPENED_AT,
+                        $queryBuilder->createNamedParameter($dateTime, Types::DATETIME_IMMUTABLE)
+                    )
+                )->executeStatement();
+        } catch (Throwable $exception) {
+            $message = sprintf(
+                'Error executing query to delete old authentication events. Error was: %s.',
+                $exception->getMessage()
+            );
+            throw new StoreException($message, (int)$exception->getCode(), $exception);
+        }
+    }
 }
diff --git a/src/Stores/Interfaces/DataStoreInterface.php b/src/Stores/Interfaces/DataStoreInterface.php
index 7ba081678b8b5ba49d54345cc5baaf91de08f4f5..6fefe9a744454ade4ae0ffecdcb4dd045589ea01 100644
--- a/src/Stores/Interfaces/DataStoreInterface.php
+++ b/src/Stores/Interfaces/DataStoreInterface.php
@@ -24,4 +24,6 @@ interface DataStoreInterface extends StoreInterface
     public function getConnectedOrganizations(string $userIdentifierHashSha256): ConnectedServiceProvider\Bag;
 
     public function getActivity(string $userIdentifierHashSha256, int $maxResults, int $firstResult): Activity\Bag;
+
+    public function deleteDataOlderThan(\DateTimeImmutable $dateTime): void;
 }
diff --git a/src/Stores/Jobs/PhpRedis/RedisStore.php b/src/Stores/Jobs/PhpRedis/RedisStore.php
index a2cfbfc60d024d84dfcb90a17aa6d38da92eabdb..20e6003b132f335fb3b78c26d606fe24c6372a44 100644
--- a/src/Stores/Jobs/PhpRedis/RedisStore.php
+++ b/src/Stores/Jobs/PhpRedis/RedisStore.php
@@ -137,6 +137,7 @@ class RedisStore extends AbstractStore implements JobsStoreInterface
 
     /**
      * @throws StoreException
+     * @codeCoverageIgnore
      */
     public static function build(
         ModuleConfiguration $moduleConfiguration,
diff --git a/src/Trackers/Authentication/DoctrineDbal/Versioned/Tracker.php b/src/Trackers/Authentication/DoctrineDbal/Versioned/Tracker.php
index e974e9a2243f587339f2f09c586e04646443bdef..9c1971a0ebdd4fb1de97db1bc0631ab1904cff99 100644
--- a/src/Trackers/Authentication/DoctrineDbal/Versioned/Tracker.php
+++ b/src/Trackers/Authentication/DoctrineDbal/Versioned/Tracker.php
@@ -8,6 +8,7 @@ use Psr\Log\LoggerInterface;
 use SimpleSAML\Module\accounting\Entities\Activity;
 use SimpleSAML\Module\accounting\Entities\Authentication\Event;
 use SimpleSAML\Module\accounting\Entities\ConnectedServiceProvider;
+use SimpleSAML\Module\accounting\Exceptions\InvalidConfigurationException;
 use SimpleSAML\Module\accounting\ModuleConfiguration;
 use SimpleSAML\Module\accounting\Providers\Interfaces\AuthenticationDataProviderInterface;
 use SimpleSAML\Module\accounting\Services\HelpersManager;
@@ -84,4 +85,22 @@ class Tracker implements AuthenticationDataTrackerInterface, AuthenticationDataP
         $userIdentifierHashSha256 = $this->helpersManager->getHashHelper()->getSha256($userIdentifier);
         return $this->dataStore->getActivity($userIdentifierHashSha256, $maxResults, $firstResult);
     }
+
+    public function enforceDataRetentionPolicy(\DateInterval $retentionPolicy): void
+    {
+        $dateTime = (new \DateTimeImmutable())->sub($retentionPolicy);
+
+        if ($dateTime === false) {
+            // @codeCoverageIgnoreStart
+            $message = sprintf(
+                'Could not create DateTime instance for data retention policy enforcement. Retention policy was: %s.',
+                var_export($retentionPolicy, true)
+            );
+            $this->logger->error($message);
+            throw new InvalidConfigurationException($message);
+            // @codeCoverageIgnoreEnd
+        }
+
+        $this->dataStore->deleteDataOlderThan($dateTime);
+    }
 }
diff --git a/src/Trackers/Interfaces/AuthenticationDataTrackerInterface.php b/src/Trackers/Interfaces/AuthenticationDataTrackerInterface.php
index b3b95720279ab21603920b762477553244593833..9a1f826bd2420a0701b2289de541c2c89dddfd73 100644
--- a/src/Trackers/Interfaces/AuthenticationDataTrackerInterface.php
+++ b/src/Trackers/Interfaces/AuthenticationDataTrackerInterface.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace SimpleSAML\Module\accounting\Trackers\Interfaces;
 
 use Psr\Log\LoggerInterface;
@@ -14,4 +16,6 @@ interface AuthenticationDataTrackerInterface extends BuildableUsingModuleConfigu
     public static function build(ModuleConfiguration $moduleConfiguration, LoggerInterface $logger): self;
 
     public function process(Event $authenticationEvent): void;
+
+    public function enforceDataRetentionPolicy(\DateInterval $retentionPolicy): void;
 }
diff --git a/templates/admin/configuration/status.twig b/templates/admin/configuration/status.twig
index 43ea6c53d089f104310d703b96d55d3e35beeee6..9927f73ac568730751181d03c3fd9075b326c5b5 100644
--- a/templates/admin/configuration/status.twig
+++ b/templates/admin/configuration/status.twig
@@ -28,10 +28,20 @@
                 <strong>{{ 'Tracker and Provider Setup Needed'|trans }}</strong>:
                 {{ defaultDataTrackerAndProvider.needsSetup ? 'Yes'|trans : 'No'|trans }}
             </li>
+            {% if additionalTrackers is not empty %}
+                <li>
+                    <strong>{{ 'Additional Trackers and setup'|trans }}</strong>:
+                    <ul>
+                        {% for trackerClass, trackerInstance in additionalTrackers %}
+                            <li>
+                                {{ trackerClass }}: {{ trackerInstance.needsSetup ? 'Yes'|trans : 'No'|trans }}
+                            </li>
+                        {% endfor %}
+                    </ul>
+                </li>
+            {% endif %}
         </ul>
 
-{#        TODO mivanci setup for additional trackers#}
-
         {% if moduleConfiguration.getAccountingProcessingType == 'asynchronous' %}
             <ul>
                 <li>
diff --git a/tests/config-templates/module_accounting.php b/tests/config-templates/module_accounting.php
index 82daf2b42edd594c4368cf2efeb6c4019d02ff5d..882170befdeb1d19e4908df67adb8402c21eee02 100644
--- a/tests/config-templates/module_accounting.php
+++ b/tests/config-templates/module_accounting.php
@@ -16,8 +16,6 @@ $config = [
     ModuleConfiguration::OPTION_ACCOUNTING_PROCESSING_TYPE =>
         ModuleConfiguration\AccountingProcessingType::VALUE_SYNCHRONOUS,
 
-    ModuleConfiguration::OPTION_CRON_TAG_FOR_JOB_RUNNER => 'accounting_job_runner',
-
     ModuleConfiguration::OPTION_JOBS_STORE => Stores\Jobs\DoctrineDbal\Store::class,
 
     ModuleConfiguration::OPTION_DEFAULT_DATA_TRACKER_AND_PROVIDER =>
@@ -53,4 +51,11 @@ $config = [
     ModuleConfiguration::OPTION_JOB_RUNNER_MAXIMUM_EXECUTION_TIME => null,
 
     ModuleConfiguration::OPTION_JOB_RUNNER_SHOULD_PAUSE_AFTER_NUMBER_OF_JOBS_PROCESSED => 10,
+
+    ModuleConfiguration::OPTION_TRACKER_DATA_RETENTION_POLICY => null,
+
+    ModuleConfiguration::OPTION_CRON_TAG_FOR_TRACKER_DATA_RETENTION_POLICY =>
+        'accounting_tracker_data_retention_policy',
+
+    ModuleConfiguration::OPTION_CRON_TAG_FOR_JOB_RUNNER => 'accounting_job_runner',
 ];
diff --git a/tests/src/ModuleConfigurationTest.php b/tests/src/ModuleConfigurationTest.php
index db2a3d286e794583904a4fa4272b0649f3432ef9..50bc22290cc94da2e0769762d596448b3f460254 100644
--- a/tests/src/ModuleConfigurationTest.php
+++ b/tests/src/ModuleConfigurationTest.php
@@ -324,4 +324,29 @@ class ModuleConfigurationTest extends TestCase
             ]
         );
     }
+
+    public function testThrowsForInvalidTrackerDataRetentionPolicy(): void
+    {
+        $this->expectException(InvalidConfigurationException::class);
+
+        new ModuleConfiguration(
+            null,
+            [
+                ModuleConfiguration::OPTION_TRACKER_DATA_RETENTION_POLICY => 'invalid'
+            ]
+        );
+    }
+
+    public function testThrowsForInvalidCronTagForTrackerDataRetentionPolicy(): void
+    {
+        $this->expectException(InvalidConfigurationException::class);
+
+        new ModuleConfiguration(
+            null,
+            [
+                ModuleConfiguration::OPTION_TRACKER_DATA_RETENTION_POLICY => 'P1D',
+                ModuleConfiguration::OPTION_CRON_TAG_FOR_TRACKER_DATA_RETENTION_POLICY => false,
+            ]
+        );
+    }
 }
diff --git a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RepositoryTest.php b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RepositoryTest.php
index ef1b0e1b96e6f6152dc7aa64b2f9a3b11baa92ac..63144f4399de5686a7c2eb91673614d5cb7e6018 100644
--- a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RepositoryTest.php
+++ b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RepositoryTest.php
@@ -727,4 +727,63 @@ class RepositoryTest extends TestCase
 
         $repository->getActivity($this->userIdentifierHash, 10, 0);
     }
+
+    public function testCanDeleteAuthenticationEventsOlderThan(): void
+    {
+        $this->repository->insertIdp($this->idpEntityId, $this->idpEntityIdHash, $this->createdAt);
+        $idpResult = $this->repository->getIdp($this->idpEntityIdHash)->fetchAssociative();
+        $idpId = (int)$idpResult[Store\TableConstants::TABLE_IDP_COLUMN_NAME_ID];
+        $this->repository->insertIdpVersion($idpId, $this->idpMetadata, $this->idpMetadataHash, $this->createdAt);
+        $idpVersionResult = $this->repository->getIdpVersion($idpId, $this->idpMetadataHash)->fetchAssociative();
+
+        $this->repository->insertSp($this->spEntityId, $this->spEntityIdHash, $this->createdAt);
+        $spResult = $this->repository->getSp($this->spEntityIdHash)->fetchAssociative();
+        $spId = (int)$spResult[Store\TableConstants::TABLE_SP_COLUMN_NAME_ID];
+        $this->repository->insertSpVersion($spId, $this->spMetadata, $this->spMetadataHash, $this->createdAt);
+        $spVersionResult = $this->repository->getSpVersion($spId, $this->spMetadataHash)->fetchAssociative();
+
+        $this->repository->insertUser($this->userIdentifier, $this->userIdentifierHash, $this->createdAt);
+        $userResult = $this->repository->getUser($this->userIdentifierHash)->fetchAssociative();
+        $userId = (int)$userResult[Store\TableConstants::TABLE_USER_COLUMN_NAME_ID];
+        $this->repository
+            ->insertUserVersion($userId, $this->userAttributes, $this->userAttributesHash, $this->createdAt);
+        $userVersionResult = $this->repository->getUserVersion($userId, $this->userAttributesHash)->fetchAssociative();
+
+        $idpVersionId = (int)$idpVersionResult[Store\TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_ID];
+        $spVersionId = (int)$spVersionResult[Store\TableConstants::TABLE_SP_VERSION_COLUMN_NAME_ID];
+        $userVersionId = (int)$userVersionResult[Store\TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ID];
+
+        $this->repository->insertIdpSpUserVersion($idpVersionId, $spVersionId, $userVersionId, $this->createdAt);
+        $idpSpUserVersionResult = $this->repository->getIdpSpUserVersion($idpVersionId, $spVersionId, $userVersionId)
+            ->fetchAssociative();
+
+        $idpSpUserVersionId =
+            (int)$idpSpUserVersionResult[Store\TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_ID];
+
+        $this->repository->insertAuthenticationEvent(
+            $idpSpUserVersionId,
+            $this->createdAt,
+            $this->clientIpAddress,
+            $this->createdAt
+        );
+
+        $resultArray = $this->repository->getActivity($this->userIdentifierHash, 10, 0);
+        $this->assertCount(1, $resultArray);
+
+        $dateTimeInFuture = $this->createdAt->add(new \DateInterval('P1D'));
+
+        $this->repository->deleteAuthenticationEventsOlderThan($dateTimeInFuture);
+
+        $resultArray = $this->repository->getActivity($this->userIdentifierHash, 10, 0);
+        $this->assertCount(0, $resultArray);
+    }
+
+    public function testDeleteAuthenticationEventsOlderThanThrowsOnInvalidDbal(): void
+    {
+        $this->connectionStub->method('dbal')->willThrowException(new \Exception('test'));
+        $repository = new Repository($this->connectionStub, $this->loggerStub);
+        $this->expectException(StoreException::class);
+
+        $repository->deleteAuthenticationEventsOlderThan(new \DateTimeImmutable());
+    }
 }
diff --git a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/StoreTest.php b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/StoreTest.php
index e1aa07db55a6f0a8c6ff01f17b3d04ef2aeb035e..8910b95cb28b90cb2d2387dd057e7d13d929e110 100644
--- a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/StoreTest.php
+++ b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/StoreTest.php
@@ -69,15 +69,15 @@ class StoreTest extends TestCase
     protected Event $authenticationEvent;
     protected Store\HashDecoratedState $hashDecoratedState;
     /**
-     * @var \PHPUnit\Framework\MockObject\Stub|Store\Repository|Store\Repository&\PHPUnit\Framework\MockObject\Stub
+     * @var \PHPUnit\Framework\MockObject\MockObject|Store\Repository
      */
-    protected $repositoryStub;
+    protected $repositoryMock;
     /**
-     * @var Result|Result&\PHPUnit\Framework\MockObject\Stub|\PHPUnit\Framework\MockObject\Stub
+     * @var Result|\PHPUnit\Framework\MockObject\Stub
      */
     protected $resultStub;
     /**
-     * @var \PHPUnit\Framework\MockObject\MockObject|LoggerInterface|LoggerInterface&\PHPUnit\Framework\MockObject\MockObject
+     * @var \PHPUnit\Framework\MockObject\MockObject|LoggerInterface
      */
     protected $loggerMock;
 
@@ -105,7 +105,7 @@ class StoreTest extends TestCase
         $this->authenticationEvent = new Event($this->state);
 
         $this->hashDecoratedState = new Store\HashDecoratedState($this->state);
-        $this->repositoryStub = $this->createStub(Store\Repository::class);
+        $this->repositoryMock = $this->createMock(Store\Repository::class);
 
         $this->resultStub = $this->createStub(Result::class);
     }
@@ -240,7 +240,7 @@ class StoreTest extends TestCase
 
     public function testResolveIdpIdThrowsOnFirstGetIdpFailure(): void
     {
-        $this->repositoryStub->method('getIdp')->willThrowException(new \Exception('test'));
+        $this->repositoryMock->method('getIdp')->willThrowException(new \Exception('test'));
         /** @psalm-suppress InvalidArgument */
         $store = new Store(
             $this->moduleConfigurationStub,
@@ -248,7 +248,7 @@ class StoreTest extends TestCase
             null,
             ModuleConfiguration\ConnectionType::MASTER,
             $this->factoryStub,
-            $this->repositoryStub
+            $this->repositoryMock
         );
 
         $this->expectException(StoreException::class);
@@ -259,8 +259,8 @@ class StoreTest extends TestCase
     public function testResolveIdpIdThrowsOnInsertAndGetIdpFailure(): void
     {
         $this->resultStub->method('fetchOne')->willReturn(false);
-        $this->repositoryStub->method('getIdp')->willReturn($this->resultStub);
-        $this->repositoryStub->method('insertIdp')->willThrowException(new \Exception('test'));
+        $this->repositoryMock->method('getIdp')->willReturn($this->resultStub);
+        $this->repositoryMock->method('insertIdp')->willThrowException(new \Exception('test'));
 
         /** @psalm-suppress InvalidArgument */
         $store = new Store(
@@ -269,7 +269,7 @@ class StoreTest extends TestCase
             null,
             ModuleConfiguration\ConnectionType::MASTER,
             $this->factoryStub,
-            $this->repositoryStub
+            $this->repositoryMock
         );
 
         $this->expectException(StoreException::class);
@@ -280,7 +280,7 @@ class StoreTest extends TestCase
 
     public function testResolveIdpVersionIdThrowsOnFirstGetIdpVersionFailure(): void
     {
-        $this->repositoryStub->method('getIdpVersion')->willThrowException(new \Exception('test'));
+        $this->repositoryMock->method('getIdpVersion')->willThrowException(new \Exception('test'));
         /** @psalm-suppress InvalidArgument */
         $store = new Store(
             $this->moduleConfigurationStub,
@@ -288,7 +288,7 @@ class StoreTest extends TestCase
             null,
             ModuleConfiguration\ConnectionType::MASTER,
             $this->factoryStub,
-            $this->repositoryStub
+            $this->repositoryMock
         );
 
         $this->expectException(StoreException::class);
@@ -299,8 +299,8 @@ class StoreTest extends TestCase
     public function testResolveIdpVersionIdThrowsOnInsertAndGetIdpVersionFailure(): void
     {
         $this->resultStub->method('fetchOne')->willReturn(false);
-        $this->repositoryStub->method('getIdpVersion')->willReturn($this->resultStub);
-        $this->repositoryStub->method('insertIdpVersion')->willThrowException(new \Exception('test'));
+        $this->repositoryMock->method('getIdpVersion')->willReturn($this->resultStub);
+        $this->repositoryMock->method('insertIdpVersion')->willThrowException(new \Exception('test'));
 
         /** @psalm-suppress InvalidArgument */
         $store = new Store(
@@ -309,7 +309,7 @@ class StoreTest extends TestCase
             null,
             ModuleConfiguration\ConnectionType::MASTER,
             $this->factoryStub,
-            $this->repositoryStub
+            $this->repositoryMock
         );
 
         $this->expectException(StoreException::class);
@@ -320,7 +320,7 @@ class StoreTest extends TestCase
 
     public function testResolveSpIdThrowsOnFirstGetSpFailure(): void
     {
-        $this->repositoryStub->method('getSp')->willThrowException(new \Exception('test'));
+        $this->repositoryMock->method('getSp')->willThrowException(new \Exception('test'));
         /** @psalm-suppress InvalidArgument */
         $store = new Store(
             $this->moduleConfigurationStub,
@@ -328,7 +328,7 @@ class StoreTest extends TestCase
             null,
             ModuleConfiguration\ConnectionType::MASTER,
             $this->factoryStub,
-            $this->repositoryStub
+            $this->repositoryMock
         );
 
         $this->expectException(StoreException::class);
@@ -339,8 +339,8 @@ class StoreTest extends TestCase
     public function testResolveSpIdThrowsOnInsertAndGetSpFailure(): void
     {
         $this->resultStub->method('fetchOne')->willReturn(false);
-        $this->repositoryStub->method('getSp')->willReturn($this->resultStub);
-        $this->repositoryStub->method('insertSp')->willThrowException(new \Exception('test'));
+        $this->repositoryMock->method('getSp')->willReturn($this->resultStub);
+        $this->repositoryMock->method('insertSp')->willThrowException(new \Exception('test'));
 
         /** @psalm-suppress InvalidArgument */
         $store = new Store(
@@ -349,7 +349,7 @@ class StoreTest extends TestCase
             null,
             ModuleConfiguration\ConnectionType::MASTER,
             $this->factoryStub,
-            $this->repositoryStub
+            $this->repositoryMock
         );
 
         $this->expectException(StoreException::class);
@@ -360,7 +360,7 @@ class StoreTest extends TestCase
 
     public function testResolveSpVersionIdThrowsOnFirstGetSpVersionFailure(): void
     {
-        $this->repositoryStub->method('getSpVersion')->willThrowException(new \Exception('test'));
+        $this->repositoryMock->method('getSpVersion')->willThrowException(new \Exception('test'));
         /** @psalm-suppress InvalidArgument */
         $store = new Store(
             $this->moduleConfigurationStub,
@@ -368,7 +368,7 @@ class StoreTest extends TestCase
             null,
             ModuleConfiguration\ConnectionType::MASTER,
             $this->factoryStub,
-            $this->repositoryStub
+            $this->repositoryMock
         );
 
         $this->expectException(StoreException::class);
@@ -379,8 +379,8 @@ class StoreTest extends TestCase
     public function testResolveSpVersionIdThrowsOnInsertAndGetSpVersionFailure(): void
     {
         $this->resultStub->method('fetchOne')->willReturn(false);
-        $this->repositoryStub->method('getSpVersion')->willReturn($this->resultStub);
-        $this->repositoryStub->method('insertSpVersion')->willThrowException(new \Exception('test'));
+        $this->repositoryMock->method('getSpVersion')->willReturn($this->resultStub);
+        $this->repositoryMock->method('insertSpVersion')->willThrowException(new \Exception('test'));
 
         /** @psalm-suppress InvalidArgument */
         $store = new Store(
@@ -389,7 +389,7 @@ class StoreTest extends TestCase
             null,
             ModuleConfiguration\ConnectionType::MASTER,
             $this->factoryStub,
-            $this->repositoryStub
+            $this->repositoryMock
         );
 
         $this->expectException(StoreException::class);
@@ -410,7 +410,7 @@ class StoreTest extends TestCase
             null,
             ModuleConfiguration\ConnectionType::MASTER,
             $this->factoryStub,
-            $this->repositoryStub
+            $this->repositoryMock
         );
 
         $this->expectException(UnexpectedValueException::class);
@@ -420,7 +420,7 @@ class StoreTest extends TestCase
 
     public function testResolveUserIdThrowsOnFirstGetUserFailure(): void
     {
-        $this->repositoryStub->method('getUser')->willThrowException(new \Exception('test'));
+        $this->repositoryMock->method('getUser')->willThrowException(new \Exception('test'));
         /** @psalm-suppress InvalidArgument */
         $store = new Store(
             $this->moduleConfigurationStub,
@@ -428,7 +428,7 @@ class StoreTest extends TestCase
             null,
             ModuleConfiguration\ConnectionType::MASTER,
             $this->factoryStub,
-            $this->repositoryStub
+            $this->repositoryMock
         );
 
         $this->expectException(StoreException::class);
@@ -439,8 +439,8 @@ class StoreTest extends TestCase
     public function testResolveUserIdThrowsOnInsertAndGetUserFailure(): void
     {
         $this->resultStub->method('fetchOne')->willReturn(false);
-        $this->repositoryStub->method('getUser')->willReturn($this->resultStub);
-        $this->repositoryStub->method('insertUser')->willThrowException(new \Exception('test'));
+        $this->repositoryMock->method('getUser')->willReturn($this->resultStub);
+        $this->repositoryMock->method('insertUser')->willThrowException(new \Exception('test'));
 
         /** @psalm-suppress InvalidArgument */
         $store = new Store(
@@ -449,7 +449,7 @@ class StoreTest extends TestCase
             null,
             ModuleConfiguration\ConnectionType::MASTER,
             $this->factoryStub,
-            $this->repositoryStub
+            $this->repositoryMock
         );
 
         $this->expectException(StoreException::class);
@@ -460,7 +460,7 @@ class StoreTest extends TestCase
 
     public function testResolveUserVersionIdThrowsOnFirstGetUserVersionFailure(): void
     {
-        $this->repositoryStub->method('getUserVersion')->willThrowException(new \Exception('test'));
+        $this->repositoryMock->method('getUserVersion')->willThrowException(new \Exception('test'));
         /** @psalm-suppress InvalidArgument */
         $store = new Store(
             $this->moduleConfigurationStub,
@@ -468,7 +468,7 @@ class StoreTest extends TestCase
             null,
             ModuleConfiguration\ConnectionType::MASTER,
             $this->factoryStub,
-            $this->repositoryStub
+            $this->repositoryMock
         );
 
         $this->expectException(StoreException::class);
@@ -479,8 +479,8 @@ class StoreTest extends TestCase
     public function testResolveUserVersionIdThrowsOnInsertAndGetUserVersionFailure(): void
     {
         $this->resultStub->method('fetchOne')->willReturn(false);
-        $this->repositoryStub->method('getUserVersion')->willReturn($this->resultStub);
-        $this->repositoryStub->method('insertUserVersion')->willThrowException(new \Exception('test'));
+        $this->repositoryMock->method('getUserVersion')->willReturn($this->resultStub);
+        $this->repositoryMock->method('insertUserVersion')->willThrowException(new \Exception('test'));
 
         /** @psalm-suppress InvalidArgument */
         $store = new Store(
@@ -489,7 +489,7 @@ class StoreTest extends TestCase
             null,
             ModuleConfiguration\ConnectionType::MASTER,
             $this->factoryStub,
-            $this->repositoryStub
+            $this->repositoryMock
         );
 
         $this->expectException(StoreException::class);
@@ -500,7 +500,7 @@ class StoreTest extends TestCase
 
     public function testResolveIdpSpUserVersionIdThrowsOnFirstGetIdpSpUserVersionFailure(): void
     {
-        $this->repositoryStub->method('getIdpSpUserVersion')->willThrowException(new \Exception('test'));
+        $this->repositoryMock->method('getIdpSpUserVersion')->willThrowException(new \Exception('test'));
         /** @psalm-suppress InvalidArgument */
         $store = new Store(
             $this->moduleConfigurationStub,
@@ -508,7 +508,7 @@ class StoreTest extends TestCase
             null,
             ModuleConfiguration\ConnectionType::MASTER,
             $this->factoryStub,
-            $this->repositoryStub
+            $this->repositoryMock
         );
 
         $this->expectException(StoreException::class);
@@ -519,8 +519,8 @@ class StoreTest extends TestCase
     public function testResolveIdpSpUserVersionIdThrowsOnInsertAndGetIdpSpUserVersionFailure(): void
     {
         $this->resultStub->method('fetchOne')->willReturn(false);
-        $this->repositoryStub->method('getIdpSpUserVersion')->willReturn($this->resultStub);
-        $this->repositoryStub->method('insertIdpSpUserVersion')->willThrowException(new \Exception('test'));
+        $this->repositoryMock->method('getIdpSpUserVersion')->willReturn($this->resultStub);
+        $this->repositoryMock->method('insertIdpSpUserVersion')->willThrowException(new \Exception('test'));
 
         /** @psalm-suppress InvalidArgument */
         $store = new Store(
@@ -529,7 +529,7 @@ class StoreTest extends TestCase
             null,
             ModuleConfiguration\ConnectionType::MASTER,
             $this->factoryStub,
-            $this->repositoryStub
+            $this->repositoryMock
         );
 
         $this->expectException(StoreException::class);
@@ -540,7 +540,7 @@ class StoreTest extends TestCase
 
     public function testGetConnectedOrganizationsReturnsEmptyBagIfNoResults(): void
     {
-        $this->repositoryStub->method('getConnectedServiceProviders')->willReturn([]);
+        $this->repositoryMock->method('getConnectedServiceProviders')->willReturn([]);
 
         /** @psalm-suppress InvalidArgument */
         $store = new Store(
@@ -549,7 +549,7 @@ class StoreTest extends TestCase
             null,
             ModuleConfiguration\ConnectionType::MASTER,
             $this->factoryStub,
-            $this->repositoryStub
+            $this->repositoryMock
         );
 
         $connectedServiceProviderBag = $store->getConnectedOrganizations('test');
@@ -559,7 +559,7 @@ class StoreTest extends TestCase
 
     public function testCanGetConnectedOrganizationsBag(): void
     {
-        $this->repositoryStub->method('getConnectedServiceProviders')
+        $this->repositoryMock->method('getConnectedServiceProviders')
             ->willReturn([RawRowResult::CONNECTED_ORGANIZATION]);
 
         /** @psalm-suppress InvalidArgument */
@@ -569,7 +569,7 @@ class StoreTest extends TestCase
             null,
             ModuleConfiguration\ConnectionType::MASTER,
             $this->factoryStub,
-            $this->repositoryStub
+            $this->repositoryMock
         );
 
         $connectedServiceProviderBag = $store->getConnectedOrganizations('test');
@@ -582,7 +582,7 @@ class StoreTest extends TestCase
         $rawResult = RawRowResult::CONNECTED_ORGANIZATION;
         unset($rawResult[TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_NUMBER_OF_AUTHENTICATIONS]);
 
-        $this->repositoryStub->method('getConnectedServiceProviders')
+        $this->repositoryMock->method('getConnectedServiceProviders')
             ->willReturn([$rawResult]);
 
         /** @psalm-suppress InvalidArgument */
@@ -592,7 +592,7 @@ class StoreTest extends TestCase
             null,
             ModuleConfiguration\ConnectionType::MASTER,
             $this->factoryStub,
-            $this->repositoryStub
+            $this->repositoryMock
         );
 
         $this->expectException(StoreException::class);
@@ -601,7 +601,7 @@ class StoreTest extends TestCase
 
     public function testGetActivityReturnsEmptyBagIfNoResults(): void
     {
-        $this->repositoryStub->method('getActivity')->willReturn([]);
+        $this->repositoryMock->method('getActivity')->willReturn([]);
 
         /** @psalm-suppress InvalidArgument */
         $store = new Store(
@@ -610,7 +610,7 @@ class StoreTest extends TestCase
             null,
             ModuleConfiguration\ConnectionType::MASTER,
             $this->factoryStub,
-            $this->repositoryStub
+            $this->repositoryMock
         );
 
         $activityBag = $store->getActivity('test', 10, 0);
@@ -620,7 +620,7 @@ class StoreTest extends TestCase
 
     public function testCanGetActivityBag(): void
     {
-        $this->repositoryStub->method('getActivity')
+        $this->repositoryMock->method('getActivity')
             ->willReturn([RawRowResult::ACTIVITY]);
 
         /** @psalm-suppress InvalidArgument */
@@ -630,7 +630,7 @@ class StoreTest extends TestCase
             null,
             ModuleConfiguration\ConnectionType::MASTER,
             $this->factoryStub,
-            $this->repositoryStub
+            $this->repositoryMock
         );
 
         $activityBag = $store->getActivity('test', 10, 0);
@@ -643,7 +643,7 @@ class StoreTest extends TestCase
         $rawResult = RawRowResult::ACTIVITY;
         unset($rawResult[TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_HAPPENED_AT]);
 
-        $this->repositoryStub->method('getActivity')
+        $this->repositoryMock->method('getActivity')
             ->willReturn([$rawResult]);
 
         /** @psalm-suppress InvalidArgument */
@@ -653,10 +653,31 @@ class StoreTest extends TestCase
             null,
             ModuleConfiguration\ConnectionType::MASTER,
             $this->factoryStub,
-            $this->repositoryStub
+            $this->repositoryMock
         );
 
         $this->expectException(StoreException::class);
         $store->getActivity('test', 10, 0);
     }
+
+    public function testCanDeleteDataOlderThan(): void
+    {
+        $dateTime = new \DateTimeImmutable();
+
+        $this->repositoryMock->expects($this->once())
+            ->method('deleteAuthenticationEventsOlderThan')
+            ->with($dateTime);
+
+        /** @psalm-suppress InvalidArgument */
+        $store = new Store(
+            $this->moduleConfigurationStub,
+            $this->loggerMock,
+            null,
+            ModuleConfiguration\ConnectionType::MASTER,
+            $this->factoryStub,
+            $this->repositoryMock
+        );
+
+        $store->deleteDataOlderThan($dateTime);
+    }
 }
diff --git a/tests/src/Trackers/Authentication/DoctrineDbal/Versioned/TrackerTest.php b/tests/src/Trackers/Authentication/DoctrineDbal/Versioned/TrackerTest.php
index 58ed6379dca8d31672ac57a1692bc9d98351ad1d..a6800b562b5af0da2cc92d900c5c5438929284fc 100644
--- a/tests/src/Trackers/Authentication/DoctrineDbal/Versioned/TrackerTest.php
+++ b/tests/src/Trackers/Authentication/DoctrineDbal/Versioned/TrackerTest.php
@@ -8,6 +8,7 @@ use Psr\Log\LoggerInterface;
 use SimpleSAML\Module\accounting\Entities\Activity;
 use SimpleSAML\Module\accounting\Entities\Authentication\Event;
 use SimpleSAML\Module\accounting\Entities\ConnectedServiceProvider;
+use SimpleSAML\Module\accounting\Exceptions\InvalidConfigurationException;
 use SimpleSAML\Module\accounting\ModuleConfiguration;
 use SimpleSAML\Module\accounting\Services\HelpersManager;
 use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store;
@@ -194,4 +195,22 @@ class TrackerTest extends TestCase
             $tracker->getActivity('test', 10, 0)
         );
     }
+
+    public function testCanEnforceDataRetentionPolicy(): void
+    {
+        $retentionPolicy = new \DateInterval('P10D');
+
+        $this->dataStoreMock->expects($this->once())
+            ->method('deleteDataOlderThan');
+
+        $tracker = new Tracker(
+            $this->moduleConfigurationStub,
+            $this->loggerMock,
+            ModuleConfiguration\ConnectionType::MASTER,
+            $this->helpersManagerStub,
+            $this->dataStoreMock
+        );
+
+        $tracker->enforceDataRetentionPolicy($retentionPolicy);
+    }
 }
diff --git a/tests/src/Trackers/Builders/AuthenticationDataTrackerBuilderTest.php b/tests/src/Trackers/Builders/AuthenticationDataTrackerBuilderTest.php
index cf3bae0c93acf0b4ca09469393c815bcbbaaab51..8a27f1a4ab3c306b1e80521439bd976068449873 100644
--- a/tests/src/Trackers/Builders/AuthenticationDataTrackerBuilderTest.php
+++ b/tests/src/Trackers/Builders/AuthenticationDataTrackerBuilderTest.php
@@ -64,6 +64,10 @@ class AuthenticationDataTrackerBuilderTest extends TestCase
             public function runSetup(): void
             {
             }
+
+            public function enforceDataRetentionPolicy(\DateInterval $retentionPolicy): void
+            {
+            }
         };
     }