diff --git a/composer.json b/composer.json
index a746b29462019d758f722b2bb95f732a2dbba331..d88308a618cf7b54713f84fa21da33ef86d4d2e5 100644
--- a/composer.json
+++ b/composer.json
@@ -30,6 +30,7 @@
         "ext-pdo_mysql": "*",
         "ext-pdo_sqlite": "*",
         "doctrine/dbal": "^3",
+        "psr/log": "^1|^2|^3",
         "simplesamlphp/composer-module-installer": "^1"
     },
     "require-dev": {
diff --git a/src/Controller/Test.php b/src/Controller/Test.php
index 1098829918603cf8d2ac0ee624573250ce031860..1ef34ed82e9fcd3689c2e14ffd59648693add681 100644
--- a/src/Controller/Test.php
+++ b/src/Controller/Test.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
 
 namespace SimpleSAML\Module\accounting\Controller;
 
+use Exception;
 use SimpleSAML\Configuration;
 use SimpleSAML\Locale\Translate;
 use SimpleSAML\Module\accounting\ModuleConfiguration;
@@ -35,7 +36,7 @@ class Test
     /**
      * @param Request $request
      * @return Template
-     * @throws \Exception
+     * @throws Exception
      */
     public function test(Request $request): Template
     {
diff --git a/src/Entities/AuthenticationEvent.php b/src/Entities/AuthenticationEvent.php
new file mode 100644
index 0000000000000000000000000000000000000000..37b349d876ed9519be0b44b3308743e0734a69e2
--- /dev/null
+++ b/src/Entities/AuthenticationEvent.php
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\accounting\Entities;
+
+use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload;
+
+class AuthenticationEvent extends AbstractPayload
+{
+    protected array $state;
+
+    public function __construct(array $state)
+    {
+        $this->state = $state;
+    }
+
+    public function getState(): array
+    {
+        return $this->state;
+    }
+}
diff --git a/src/Entities/AuthenticationEvent/Job.php b/src/Entities/AuthenticationEvent/Job.php
new file mode 100644
index 0000000000000000000000000000000000000000..51308d6fbba4657e453e3b7b4d80240897d91036
--- /dev/null
+++ b/src/Entities/AuthenticationEvent/Job.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\accounting\Entities\AuthenticationEvent;
+
+use SimpleSAML\Module\accounting\Entities\AuthenticationEvent;
+use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload;
+use SimpleSAML\Module\accounting\Entities\GenericJob;
+use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException;
+
+class Job extends GenericJob
+{
+    public function getPayload(): AuthenticationEvent
+    {
+        return $this->validatePayload($this->payload);
+    }
+
+    public function setPayload(AbstractPayload $payload): void
+    {
+        $this->payload = $this->validatePayload($payload);
+    }
+
+    protected function validatePayload(AbstractPayload $payload): AuthenticationEvent
+    {
+        if (! ($payload instanceof AuthenticationEvent)) {
+            throw new UnexpectedValueException('Job payload must be of type AuthenticationEvent.');
+        }
+
+        return $payload;
+    }
+}
diff --git a/src/Entities/Bases/AbstractJob.php b/src/Entities/Bases/AbstractJob.php
new file mode 100644
index 0000000000000000000000000000000000000000..b2f1f73968f4d3adcf622f5e06406e6e230aae25
--- /dev/null
+++ b/src/Entities/Bases/AbstractJob.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\accounting\Entities\Bases;
+
+use SimpleSAML\Module\accounting\Entities\Interfaces\JobInterface;
+
+abstract class AbstractJob implements JobInterface
+{
+    protected AbstractPayload $payload;
+
+    public function __construct(AbstractPayload $payload)
+    {
+        $this->setPayload($payload);
+    }
+
+    public function getPayload(): AbstractPayload
+    {
+        return $this->payload;
+    }
+
+    public function setPayload(AbstractPayload $payload): void
+    {
+        $this->payload = $payload;
+    }
+}
diff --git a/src/Entities/Bases/AbstractPayload.php b/src/Entities/Bases/AbstractPayload.php
new file mode 100644
index 0000000000000000000000000000000000000000..f01d96b1b555293602f2fab8a032b5373da24fb0
--- /dev/null
+++ b/src/Entities/Bases/AbstractPayload.php
@@ -0,0 +1,9 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\accounting\Entities\Bases;
+
+abstract class AbstractPayload
+{
+}
diff --git a/src/Entities/GenericJob.php b/src/Entities/GenericJob.php
new file mode 100644
index 0000000000000000000000000000000000000000..03aa70051f005bde1e63e11b062d61c9449702f8
--- /dev/null
+++ b/src/Entities/GenericJob.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace SimpleSAML\Module\accounting\Entities;
+
+class GenericJob extends Bases\AbstractJob
+{
+}
diff --git a/src/Entities/Interfaces/JobInterface.php b/src/Entities/Interfaces/JobInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..c4622c3b09ced9fa1547a945759d6f9bfb7aba6a
--- /dev/null
+++ b/src/Entities/Interfaces/JobInterface.php
@@ -0,0 +1,14 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\accounting\Entities\Interfaces;
+
+use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload;
+
+interface JobInterface
+{
+    public function getPayload(): AbstractPayload;
+
+    public function setPayload(AbstractPayload $payload): void;
+}
diff --git a/src/Exceptions/InvalidConfigurationException.php b/src/Exceptions/InvalidConfigurationException.php
index 24cf0ebad073769cb93f711c7c3cabfca2d6c85a..034624ec83e27d795f703a7d66b92f57f75cbe99 100644
--- a/src/Exceptions/InvalidConfigurationException.php
+++ b/src/Exceptions/InvalidConfigurationException.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
 
 namespace SimpleSAML\Module\accounting\Exceptions;
 
-class InvalidConfigurationException extends \ValueError
+use ValueError;
+
+class InvalidConfigurationException extends ValueError
 {
 }
diff --git a/src/Exceptions/InvalidValueException.php b/src/Exceptions/InvalidValueException.php
index 81be77ef32ec586e4f48c1eafa3fd6a54e5f9d58..c4932850a481b43fa43d431281074d553bcdaedf 100644
--- a/src/Exceptions/InvalidValueException.php
+++ b/src/Exceptions/InvalidValueException.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
 
 namespace SimpleSAML\Module\accounting\Exceptions;
 
-class InvalidValueException extends \ValueError
+use ValueError;
+
+class InvalidValueException extends ValueError
 {
 }
diff --git a/src/Exceptions/MigrationException.php b/src/Exceptions/StoreException.php
similarity index 61%
rename from src/Exceptions/MigrationException.php
rename to src/Exceptions/StoreException.php
index 5afd8a7d8453c6c54f320690856b1eada1aa7019..3e1b6772e307e438427d0a96a6a942031188a665 100644
--- a/src/Exceptions/MigrationException.php
+++ b/src/Exceptions/StoreException.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
 
 namespace SimpleSAML\Module\accounting\Exceptions;
 
-class MigrationException extends \Exception
+use Exception;
+
+class StoreException extends Exception
 {
 }
diff --git a/src/Exceptions/StoreException/MigrationException.php b/src/Exceptions/StoreException/MigrationException.php
new file mode 100644
index 0000000000000000000000000000000000000000..377d06efc3c13d0e68b9dc5785f4c39dfbc38b91
--- /dev/null
+++ b/src/Exceptions/StoreException/MigrationException.php
@@ -0,0 +1,11 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\accounting\Exceptions\StoreException;
+
+use SimpleSAML\Module\accounting\Exceptions\StoreException;
+
+class MigrationException extends StoreException
+{
+}
diff --git a/src/Exceptions/UnexpectedValueException.php b/src/Exceptions/UnexpectedValueException.php
new file mode 100644
index 0000000000000000000000000000000000000000..22fde53e21b31720a2faf7fb754b41659dd3bd54
--- /dev/null
+++ b/src/Exceptions/UnexpectedValueException.php
@@ -0,0 +1,9 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\accounting\Exceptions;
+
+class UnexpectedValueException extends \UnexpectedValueException
+{
+}
diff --git a/src/Helpers/FilesystemHelper.php b/src/Helpers/FilesystemHelper.php
index a33fd0ac1105e0e88627490a407b41e15dcd8d9c..074b461b287011cdfb8308351458811ad66e4b2f 100644
--- a/src/Helpers/FilesystemHelper.php
+++ b/src/Helpers/FilesystemHelper.php
@@ -10,12 +10,12 @@ class FilesystemHelper
 {
     public static function getRealPath(string $path): string
     {
-        $path = realpath($path);
+        $realpath = realpath($path);
 
-        if ($path === false || ! (is_dir($path) || is_file($path))) {
-            throw new InvalidValueException('Given path can not be translated to real path.');
+        if ($realpath === false || ! (is_dir($realpath) || is_file($realpath))) {
+            throw new InvalidValueException(sprintf('Given path can not be translated to real path (%s).', $path));
         }
 
-        return $path;
+        return $realpath;
     }
 }
diff --git a/src/Services/LoggerService.php b/src/Services/LoggerService.php
index 5dc711e67a4c2c6d20e68fa0331cce1bb956befd..11827f959d2664fa76a4cc194198ecb8259ca1c3 100644
--- a/src/Services/LoggerService.php
+++ b/src/Services/LoggerService.php
@@ -1,93 +1,51 @@
 <?php
 
+declare(strict_types=1);
+
 namespace SimpleSAML\Module\accounting\Services;
 
+use Psr\Log\AbstractLogger;
+use Psr\Log\LogLevel;
 use SimpleSAML\Logger;
 
-class LoggerService
+class LoggerService extends AbstractLogger
 {
-    /**
-     * Log an emergency message.
-     *
-     * @param string $message The message to log.
-     */
-    public function emergency(string $message): void
-    {
-        Logger::emergency($message);
-    }
-
-    /**
-     * Log a critical message.
-     *
-     * @param string $message The message to log.
-     */
-    public function critical(string $message): void
-    {
-        Logger::critical($message);
-    }
-
-    /**
-     * Log an alert message.
-     *
-     * @param string $message The message to log.
-     */
-    public function alert(string $message): void
-    {
-        Logger::alert($message);
-    }
-
-    /**
-     * Log an error message.
-     *
-     * @param string $message The message to log.
-     */
-    public function error(string $message): void
-    {
-        Logger::error($message);
-    }
-
-    /**
-     * Log a warning message.
-     *
-     * @param string $message The message to log.
-     */
-    public function warning(string $message): void
-    {
-        Logger::warning($message);
-    }
-
-    /**
-     * Log a notice message.
-     *
-     * @param string $message The message to log.
-     */
-    public function notice(string $message): void
-    {
-        Logger::notice($message);
-    }
-
-    /**
-     * Log an info message (a bit less verbose than debug messages).
-     *
-     * @param string $message The message to log.
-     */
-    public function info(string $message): void
-    {
-        Logger::info($message);
-    }
-
-    /**
-     * Log a debug message (very verbose messages).
-     *
-     * @param string $message The message to log.
-     */
-    public function debug(string $message): void
-    {
-        Logger::debug($message);
-    }
-
-    /**
-     * Log a statistics message.
+    public function log($level, $message, array $context = [])
+    {
+        if (! empty($context)) {
+            $message .= ' Context: ' . var_export($context, true);
+        }
+
+        switch ($level) {
+            case LogLevel::EMERGENCY:
+                Logger::emergency($message);
+                break;
+            case LogLevel::CRITICAL:
+                Logger::critical($message);
+                break;
+            case LogLevel::ALERT:
+                Logger::alert($message);
+                break;
+            case LogLevel::ERROR:
+                Logger::error($message);
+                break;
+            case LogLevel::WARNING:
+                Logger::warning($message);
+                break;
+            case LogLevel::NOTICE:
+                Logger::notice($message);
+                break;
+            case LogLevel::INFO:
+                Logger::info($message);
+                break;
+            case LogLevel::DEBUG:
+                Logger::debug($message);
+                break;
+        }
+    }
+
+    /**
+     * Log an SSP statistics message.
      *
      * @param string $message The message to log.
      */
diff --git a/src/Stores/Connections/Bases/AbstractMigrator.php b/src/Stores/Connections/Bases/AbstractMigrator.php
index 0a68d56f216a1236861540eea483b763f3ef43f0..613efc1b6cd5eb47853cbe0471d6b774f54f8ff8 100644
--- a/src/Stores/Connections/Bases/AbstractMigrator.php
+++ b/src/Stores/Connections/Bases/AbstractMigrator.php
@@ -5,9 +5,10 @@ declare(strict_types=1);
 namespace SimpleSAML\Module\accounting\Stores\Connections\Bases;
 
 use SimpleSAML\Module\accounting\Exceptions\InvalidValueException;
-use SimpleSAML\Module\accounting\Exceptions\MigrationException;
+use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException;
 use SimpleSAML\Module\accounting\Helpers\FilesystemHelper;
 use SimpleSAML\Module\accounting\Stores\Interfaces\MigrationInterface;
+use Throwable;
 
 abstract class AbstractMigrator
 {
@@ -49,6 +50,7 @@ abstract class AbstractMigrator
     /**
      * @param class-string[] $migrationClasses
      * @return void
+     * @throws MigrationException
      */
     public function runMigrationClasses(array $migrationClasses): void
     {
@@ -59,14 +61,14 @@ abstract class AbstractMigrator
 
             try {
                 $migration->run();
-            } catch (\Throwable $exception) {
+            } catch (Throwable $exception) {
                 $message = sprintf(
                     'Could not run migration class %s. Error was: %s',
                     $migrationClass,
                     $exception->getMessage()
                 );
 
-                throw new MigrationException($message);
+                throw new MigrationException($message, (int) $exception->getCode(), $exception);
             }
 
             $this->markImplementedMigrationClass($migrationClass);
@@ -94,6 +96,9 @@ abstract class AbstractMigrator
         return ! empty($this->getNonImplementedMigrationClasses($directory, $namespace));
     }
 
+    /**
+     * @throws MigrationException
+     */
     public function runNonImplementedMigrationClasses(string $directory, string $namespace): void
     {
         $this->runMigrationClasses($this->getNonImplementedMigrationClasses($directory, $namespace));
diff --git a/src/Stores/Connections/DoctrineDbal/Bases/AbstractMigration.php b/src/Stores/Connections/DoctrineDbal/Bases/AbstractMigration.php
index f64b7113690f1372ca2ef8ca9ec9ba9d5a4bee20..d0a8b9497ae5850509ed78b5659291a3e4b98c99 100644
--- a/src/Stores/Connections/DoctrineDbal/Bases/AbstractMigration.php
+++ b/src/Stores/Connections/DoctrineDbal/Bases/AbstractMigration.php
@@ -1,19 +1,47 @@
 <?php
 
+declare(strict_types=1);
+
 namespace SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases;
 
 use Doctrine\DBAL\Schema\AbstractSchemaManager;
+use SimpleSAML\Module\accounting\Exceptions\StoreException;
+use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException;
 use SimpleSAML\Module\accounting\Stores\Interfaces\MigrationInterface;
 use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection;
+use Throwable;
 
 abstract class AbstractMigration implements MigrationInterface
 {
     protected Connection $connection;
     protected AbstractSchemaManager $schemaManager;
 
+    /**
+     * @throws StoreException
+     */
     public function __construct(Connection $connection)
     {
         $this->connection = $connection;
-        $this->schemaManager = $this->connection->dbal()->createSchemaManager();
+        try {
+            $this->schemaManager = $this->connection->dbal()->createSchemaManager();
+        } catch (Throwable $exception) {
+            $message = 'Could not create DBAL schema manager.';
+            throw new StoreException($message, (int) $exception->getCode(), $exception);
+        }
+    }
+
+    /**
+     * @throws MigrationException
+     */
+    protected function throwGenericMigrationException(string $contextDetails, Throwable $throwable): void
+    {
+        $message = sprintf(
+            'There was an error running a migration class %s. Context details: %s. Error was: %s.',
+            static::class,
+            $contextDetails,
+            $throwable->getMessage()
+        );
+
+        throw new MigrationException($message, (int) $throwable->getCode(), $throwable);
     }
 }
diff --git a/src/Stores/Connections/DoctrineDbal/Factory.php b/src/Stores/Connections/DoctrineDbal/Factory.php
index 6c2af906f32431080113f9235d128c734de93eaf..45d9c1076bb93e4ac78425ded9c9bfcb57ab48f6 100644
--- a/src/Stores/Connections/DoctrineDbal/Factory.php
+++ b/src/Stores/Connections/DoctrineDbal/Factory.php
@@ -1,16 +1,18 @@
 <?php
 
+declare(strict_types=1);
+
 namespace SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal;
 
+use Psr\Log\LoggerInterface;
 use SimpleSAML\Module\accounting\ModuleConfiguration;
-use SimpleSAML\Module\accounting\Services\LoggerService;
 
 class Factory
 {
     protected ModuleConfiguration $moduleConfiguration;
-    protected LoggerService $loggerService;
+    protected LoggerInterface $loggerService;
 
-    public function __construct(ModuleConfiguration $moduleConfiguration, LoggerService $loggerService)
+    public function __construct(ModuleConfiguration $moduleConfiguration, LoggerInterface $loggerService)
     {
         $this->moduleConfiguration = $moduleConfiguration;
         $this->loggerService = $loggerService;
@@ -21,7 +23,7 @@ class Factory
         return new Connection($this->moduleConfiguration->getStoreConnectionParameters($connectionKey));
     }
 
-    public function buildMigrator(Connection $connection, LoggerService $loggerService = null): Migrator
+    public function buildMigrator(Connection $connection, LoggerInterface $loggerService = null): Migrator
     {
         return new Migrator($connection, $loggerService ?? $this->loggerService);
     }
diff --git a/src/Stores/Connections/DoctrineDbal/Migrator.php b/src/Stores/Connections/DoctrineDbal/Migrator.php
index 39861c7d6875690af664c705420acdd2431ff63d..e804b2876a76aa72d2e1e11d07361be25ede7c9d 100644
--- a/src/Stores/Connections/DoctrineDbal/Migrator.php
+++ b/src/Stores/Connections/DoctrineDbal/Migrator.php
@@ -1,15 +1,22 @@
 <?php
 
+declare(strict_types=1);
+
 namespace SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal;
 
+use DateTimeImmutable;
 use Doctrine\DBAL\Schema\AbstractSchemaManager;
 use Doctrine\DBAL\Schema\Table;
 use Doctrine\DBAL\Types\Types;
+use Psr\Log\LoggerInterface;
+use ReflectionClass;
+use ReflectionException;
 use SimpleSAML\Module\accounting\Exceptions\InvalidValueException;
-use SimpleSAML\Module\accounting\Services\LoggerService;
+use SimpleSAML\Module\accounting\Exceptions\StoreException;
 use SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator;
 use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration;
 use SimpleSAML\Module\accounting\Stores\Interfaces\MigrationInterface;
+use Throwable;
 
 class Migrator extends AbstractMigrator
 {
@@ -20,57 +27,87 @@ class Migrator extends AbstractMigrator
     public const COLUMN_NAME_CREATED_AT = 'created_at';
 
     protected Connection $connection;
-    protected LoggerService $loggerService;
+    protected LoggerInterface $logger;
 
     protected AbstractSchemaManager $schemaManager;
     protected string $prefixedTableName;
 
-    public function __construct(Connection $connection, LoggerService $loggerService)
+    /**
+     * @throws StoreException
+     */
+    public function __construct(Connection $connection, LoggerInterface $logger)
     {
         $this->connection = $connection;
-        $this->loggerService = $loggerService;
+        $this->logger = $logger;
 
-        $this->schemaManager = $this->connection->dbal()->createSchemaManager();
+        try {
+            $this->schemaManager = $this->connection->dbal()->createSchemaManager();
+        } catch (Throwable $exception) {
+            $message = 'Could not create DBAL schema manager.';
+            throw new StoreException($message, (int) $exception->getCode(), $exception);
+        }
         $this->prefixedTableName = $this->connection->preparePrefixedTableName(self::TABLE_NAME);
     }
 
+    /**
+     * @throws StoreException
+     */
     public function needsSetup(): bool
     {
-        return ! $this->schemaManager->tablesExist([$this->prefixedTableName]);
+        try {
+            return ! $this->schemaManager->tablesExist([$this->prefixedTableName]);
+        } catch (Throwable $exception) {
+            $message = sprintf('Could not check table %s existence using schema manager.', $this->prefixedTableName);
+            throw new StoreException($message, (int) $exception->getCode(), $exception);
+        }
     }
 
+    /**
+     * @throws StoreException
+     */
     public function runSetup(): void
     {
         if (! $this->needsSetup()) {
-            $this->loggerService->warning('Migrator setup has been called, however setup is not needed.');
+            $this->logger->warning('Migrator setup has been called, however setup is not needed.');
             return;
         }
 
         $this->createMigrationsTable();
     }
 
+    /**
+     * @throws StoreException
+     */
     protected function createMigrationsTable(): void
     {
-        $table = new Table($this->prefixedTableName);
-
-        $table->addColumn(self::COLUMN_NAME_ID, Types::BIGINT)
-            ->setAutoincrement(true)
-            ->setUnsigned(true);
-        $table->addColumn(self::COLUMN_NAME_VERSION, Types::STRING);
-        $table->addColumn(self::COLUMN_NAME_CREATED_AT, Types::DATETIMETZ_IMMUTABLE);
-
-        $table->setPrimaryKey(['id']);
-        $table->addUniqueIndex([self::COLUMN_NAME_VERSION]);
-
-        $this->schemaManager->createTable($table);
+        try {
+            $table = new Table($this->prefixedTableName);
+
+            $table->addColumn(self::COLUMN_NAME_ID, Types::BIGINT)
+                ->setAutoincrement(true)
+                ->setUnsigned(true);
+            $table->addColumn(self::COLUMN_NAME_VERSION, Types::STRING);
+            $table->addColumn(self::COLUMN_NAME_CREATED_AT, Types::DATETIMETZ_IMMUTABLE);
+
+            $table->setPrimaryKey(['id']);
+            $table->addUniqueIndex([self::COLUMN_NAME_VERSION]);
+
+            $this->schemaManager->createTable($table);
+        } catch (Throwable $exception) {
+            $message = sprintf('Error creating migrations table %s.', $this->prefixedTableName);
+            throw new StoreException($message, (int) $exception->getCode(), $exception);
+        }
     }
 
+    /**
+     * @throws ReflectionException
+     */
     protected function buildMigrationClassInstance(string $migrationClass): MigrationInterface
     {
         $this->validateDoctrineDbalMigrationClass($migrationClass);
 
         /** @var MigrationInterface $migration */
-        $migration = (new \ReflectionClass($migrationClass))->newInstance($this->connection);
+        $migration = (new ReflectionClass($migrationClass))->newInstance($this->connection);
 
         return $migration;
     }
@@ -82,40 +119,56 @@ class Migrator extends AbstractMigrator
         }
     }
 
+    /**
+     * @throws StoreException
+     */
     protected function markImplementedMigrationClass(string $migrationClass): void
     {
         $queryBuilder = $this->connection->dbal()->createQueryBuilder();
 
-        $queryBuilder->insert($this->prefixedTableName)
-            ->values(
-                [
-                    self::COLUMN_NAME_VERSION => ':' . self::COLUMN_NAME_VERSION,
-                    self::COLUMN_NAME_CREATED_AT => ':' . self::COLUMN_NAME_CREATED_AT,
-                ]
-            )
-            ->setParameters(
-                [
-                    self::COLUMN_NAME_VERSION => $migrationClass,
-                    self::COLUMN_NAME_CREATED_AT => new \DateTimeImmutable(),
-                ],
-                [
-                    self::COLUMN_NAME_VERSION => Types::STRING,
-                    self::COLUMN_NAME_CREATED_AT => Types::DATETIMETZ_IMMUTABLE
-                ]
-            );
-
-        $queryBuilder->executeStatement();
+        try {
+            $queryBuilder->insert($this->prefixedTableName)
+                ->values(
+                    [
+                        self::COLUMN_NAME_VERSION => ':' . self::COLUMN_NAME_VERSION,
+                        self::COLUMN_NAME_CREATED_AT => ':' . self::COLUMN_NAME_CREATED_AT,
+                    ]
+                )
+                ->setParameters(
+                    [
+                        self::COLUMN_NAME_VERSION => $migrationClass,
+                        self::COLUMN_NAME_CREATED_AT => new DateTimeImmutable(),
+                    ],
+                    [
+                        self::COLUMN_NAME_VERSION => Types::STRING,
+                        self::COLUMN_NAME_CREATED_AT => Types::DATETIMETZ_IMMUTABLE
+                    ]
+                );
+
+            $queryBuilder->executeStatement();
+        } catch (Throwable $exception) {
+            $message = sprintf('Error marking implemented migrations class %s.', $migrationClass);
+            throw new StoreException($message, (int) $exception->getCode(), $exception);
+        }
     }
 
+    /**
+     * @throws StoreException
+     */
     public function getImplementedMigrationClasses(): array
     {
-        $queryBuilder = $this->connection->dbal()->createQueryBuilder();
+        try {
+            $queryBuilder = $this->connection->dbal()->createQueryBuilder();
 
-        $queryBuilder->select(self::COLUMN_NAME_VERSION)
-            ->from($this->prefixedTableName);
+            $queryBuilder->select(self::COLUMN_NAME_VERSION)
+                ->from($this->prefixedTableName);
 
-        /** @var class-string[] $migrationClasses */
-        $migrationClasses = $queryBuilder->executeQuery()->fetchFirstColumn();
+            /** @var class-string[] $migrationClasses */
+            $migrationClasses = $queryBuilder->executeQuery()->fetchFirstColumn();
+        } catch (Throwable $exception) {
+            $message = 'Error getting implemented migration classes.';
+            throw new StoreException($message, (int) $exception->getCode(), $exception);
+        }
 
         return $migrationClasses;
     }
diff --git a/src/Stores/Interfaces/ConnectionInterface.php b/src/Stores/Interfaces/ConnectionInterface.php
index 359c817b5dfd45cfab9ada2daeddc0cc0801bc61..b61d3208388cd61c501b2df5d93530628f501f59 100644
--- a/src/Stores/Interfaces/ConnectionInterface.php
+++ b/src/Stores/Interfaces/ConnectionInterface.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace SimpleSAML\Module\accounting\Stores\Interfaces;
 
 interface ConnectionInterface
diff --git a/src/Stores/Interfaces/DataStoreInterface.php b/src/Stores/Interfaces/DataStoreInterface.php
index b091acf1197ead3a12f744764e8a984b9f31f911..4d06925cd631780d351bc7eca618522d7829c726 100644
--- a/src/Stores/Interfaces/DataStoreInterface.php
+++ b/src/Stores/Interfaces/DataStoreInterface.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace SimpleSAML\Module\accounting\Stores\Interfaces;
 
 interface DataStoreInterface extends StoreInterface
diff --git a/src/Stores/Interfaces/JobsStoreInterface.php b/src/Stores/Interfaces/JobsStoreInterface.php
index 3dd0b57bf05c7bc0aec7c69250f25564af415ec0..52d973bb7f08d768cc9650553059148e63f7010f 100644
--- a/src/Stores/Interfaces/JobsStoreInterface.php
+++ b/src/Stores/Interfaces/JobsStoreInterface.php
@@ -1,7 +1,25 @@
 <?php
 
+declare(strict_types=1);
+
 namespace SimpleSAML\Module\accounting\Stores\Interfaces;
 
+use SimpleSAML\Module\accounting\Entities\Interfaces\JobInterface;
+
 interface JobsStoreInterface extends StoreInterface
 {
+    /**
+     * Add job to queue
+     * @param JobInterface $job
+     * @param string|null $type Optional job type designation. If not null, payload class FQN will be used.
+     * @return void
+     */
+    public function enqueue(JobInterface $job, string $type = null): void;
+
+    /**
+     * Get job from queue
+     * @param string|null $type
+     * @return ?JobInterface
+     */
+    public function dequeue(string $type = null): ?JobInterface;
 }
diff --git a/src/Stores/Interfaces/MigrationInterface.php b/src/Stores/Interfaces/MigrationInterface.php
index ea52b88a626c4d4c69a2372e0e7158b87d989af9..ce7e523381d6e84b95720bc319baaa8e380cf12e 100644
--- a/src/Stores/Interfaces/MigrationInterface.php
+++ b/src/Stores/Interfaces/MigrationInterface.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace SimpleSAML\Module\accounting\Stores\Interfaces;
 
 interface MigrationInterface
diff --git a/src/Stores/Interfaces/StoreInterface.php b/src/Stores/Interfaces/StoreInterface.php
index 082ee53080fce20782c10fd04a26243b1080f292..ea80efa40131b993caac696176123e5a7d1544a4 100644
--- a/src/Stores/Interfaces/StoreInterface.php
+++ b/src/Stores/Interfaces/StoreInterface.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace SimpleSAML\Module\accounting\Stores\Interfaces;
 
 interface StoreInterface
diff --git a/src/Stores/Jobs/DoctrineDbal/JobsStore.php b/src/Stores/Jobs/DoctrineDbal/JobsStore.php
index 25590c8883b91bb05c460a5164277bb562ea951d..30f760d5bf4f352a6925c02a5d4881276f262dcf 100644
--- a/src/Stores/Jobs/DoctrineDbal/JobsStore.php
+++ b/src/Stores/Jobs/DoctrineDbal/JobsStore.php
@@ -4,37 +4,249 @@ declare(strict_types=1);
 
 namespace SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal;
 
+use DateTimeImmutable;
+use Doctrine\DBAL\Result;
+use Doctrine\DBAL\Types\Types;
+use Psr\Log\LoggerInterface;
+use ReflectionClass;
+use SimpleSAML\Module\accounting\Entities\GenericJob;
+use SimpleSAML\Module\accounting\Entities\Interfaces\JobInterface;
+use SimpleSAML\Module\accounting\Exceptions\StoreException;
+use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException;
 use SimpleSAML\Module\accounting\ModuleConfiguration;
-use SimpleSAML\Module\accounting\Stores\Jobs\Interfaces\JobsStoreInterface;
+use SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator;
 use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection;
 use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Factory;
 use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator;
+use SimpleSAML\Module\accounting\Stores\Interfaces\JobsStoreInterface;
+use Throwable;
 
 class JobsStore implements JobsStoreInterface
 {
     public const TABLE_NAME_JOBS = 'jobs';
     public const TABLE_NAME_FAILED_JOBS = 'failed_jobs';
 
+    public const COLUMN_NAME_ID = 'id';
+    public const COLUMN_NAME_PAYLOAD = 'payload';
+    public const COLUMN_NAME_TYPE = 'type';
+    public const COLUMN_NAME_CREATED_AT = 'created_at';
+
+    public const COLUMN_LENGTH_TYPE = 1024;
+
     protected ModuleConfiguration $moduleConfiguration;
     protected Connection $connection;
     protected string $prefixedTableNameJobs;
     protected string $prefixedTableNameFailedJobs;
     protected Migrator $migrator;
+    protected LoggerInterface $logger;
 
-    public function __construct(ModuleConfiguration $moduleConfiguration, Factory $factory)
-    {
+    public function __construct(
+        ModuleConfiguration $moduleConfiguration,
+        Factory $factory,
+        LoggerInterface $logger
+    ) {
         $this->moduleConfiguration = $moduleConfiguration;
         $this->connection = $factory->buildConnection($moduleConfiguration->getStoreConnection(self::class));
         $this->migrator = $factory->buildMigrator($this->connection);
+        $this->logger = $logger;
 
         $this->prefixedTableNameJobs = $this->connection->preparePrefixedTableName(self::TABLE_NAME_JOBS);
         $this->prefixedTableNameFailedJobs = $this->connection->preparePrefixedTableName(self::TABLE_NAME_FAILED_JOBS);
     }
 
-    public function needsSetUp(): bool
+    /**
+     * @throws StoreException
+     */
+    public function enqueue(JobInterface $job, string $type = null): void
+    {
+        $queryBuilder = $this->connection->dbal()->createQueryBuilder();
+
+        $payload = $job->getPayload();
+        $type = $this->validateType($type ?? get_class($job));
+
+        $queryBuilder->insert($this->prefixedTableNameJobs)
+            ->values(
+                [
+                    self::COLUMN_NAME_PAYLOAD => ':' . self::COLUMN_NAME_PAYLOAD,
+                    self::COLUMN_NAME_TYPE => ':' . self::COLUMN_NAME_TYPE,
+                    self::COLUMN_NAME_CREATED_AT => ':' . self::COLUMN_NAME_CREATED_AT,
+                ]
+            )
+            ->setParameters(
+                [
+                    self::COLUMN_NAME_PAYLOAD => serialize($payload),
+                    self::COLUMN_NAME_TYPE => $type,
+                    self::COLUMN_NAME_CREATED_AT => new DateTimeImmutable(),
+                ],
+                [
+                    self::COLUMN_NAME_PAYLOAD => Types::TEXT,
+                    self::COLUMN_NAME_TYPE => Types::STRING,
+                    self::COLUMN_NAME_CREATED_AT => Types::DATETIMETZ_IMMUTABLE
+                ]
+            );
+
+        try {
+            $queryBuilder->executeStatement();
+        } catch (Throwable $exception) {
+            $message = sprintf('Could not enqueue job (%s)', $exception->getMessage());
+            throw new StoreException($message, (int)$exception->getCode(), $exception);
+        }
+    }
+
+    /**
+     * @throws StoreException
+     */
+    protected function validateType(string $type): string
+    {
+        if (mb_strlen($type) > self::COLUMN_LENGTH_TYPE) {
+            throw new StoreException(
+                sprintf('String length for type column exceeds %s limit.', self::COLUMN_LENGTH_TYPE)
+            );
+        }
+
+        return $type;
+    }
+
+    /**
+     * @throws StoreException
+     */
+    public function dequeue(string $type = null): ?JobInterface
+    {
+        $job = null;
+
+        try {
+            // Check if there are any jobs in the store...
+            while ($row = $this->getNext($type)->fetchAssociative()) {
+                // We have a row. Let's create a row job instance, which will take care of validation and make it easier
+                // to work with instead of array.
+                $rawJob = new RawJob($row, $this->connection->dbal()->getDatabasePlatform());
+                // Let's try to delete this job from the store, so it can't be fetched again.
+                $numberOfAffectedRows = $this->delete($rawJob->getId());
+                if ($numberOfAffectedRows === 0) {
+                    // It seems that this job has already been dequeued in the meantime. Try to get next job again.
+                    continue;
+                }
+                // Job is deleted, meaning it is now dequeued and we can return a new job instance.
+                // If a valid type is declared, let's try to instantiate a job of that specific type.
+                if ($type !== null && class_exists($type) && is_subclass_of($type, JobInterface::class)) {
+                    $job = (new ReflectionClass($type))->newInstance($rawJob->getPayload());
+                } else {
+                    // No (valid) job type, so generic job will do...
+                    $job = new GenericJob($rawJob->getPayload());
+                }
+
+                // We have found and dequeued a job, so finish with the search.
+                break;
+            }
+        } catch (Throwable $exception) {
+            throw new StoreException(
+                'Error while trying to dequeue a job.',
+                (int) $exception->getCode(),
+                $exception
+            );
+        }
+
+        return $job;
+    }
+
+    /**
+     * @throws StoreException
+     */
+    public function getNext(string $type = null): Result
+    {
+        $queryBuilder = $this->connection->dbal()->createQueryBuilder();
+
+        /**
+         * @psalm-suppress TooManyArguments - providing array or null is deprecated
+         */
+        $queryBuilder->select(
+            self::COLUMN_NAME_ID,
+            self::COLUMN_NAME_PAYLOAD,
+            self::COLUMN_NAME_TYPE,
+            self::COLUMN_NAME_CREATED_AT
+        )
+            ->from($this->prefixedTableNameJobs)
+            ->orderBy(self::COLUMN_NAME_ID)
+            ->setMaxResults(1);
+
+        if ($type !== null) {
+            $queryBuilder->where(self::COLUMN_NAME_TYPE . ' = ' . $queryBuilder->createNamedParameter($type));
+        }
+
+        try {
+            $result = $queryBuilder->executeQuery();
+        } catch (Throwable $exception) {
+            $message = 'Error while trying to execute query to get next available job.';
+            throw new StoreException($message, (int)$exception->getCode(), $exception);
+        }
+
+        return $result;
+    }
+
+    protected function delete(int $id): int
+    {
+        return (int)$this->connection->dbal()
+            ->delete(
+                $this->prefixedTableNameJobs,
+                [self::COLUMN_NAME_ID => $id],
+                [self::COLUMN_NAME_ID => Types::BIGINT]
+            );
+    }
+
+    /**
+     * @throws StoreException
+     * @throws MigrationException
+     */
+    public function runSetup(): void
+    {
+        if ($this->migrator->needsSetup()) {
+            $this->migrator->runSetup();
+        }
+
+        if (!$this->areAllMigrationsImplemented()) {
+            $this->migrator->runNonImplementedMigrationClasses(
+                $this->getMigrationsDirectory(),
+                $this->getMigrationsNamespace()
+            );
+        }
+    }
+
+    /**
+     * @throws StoreException
+     */
+    public function needsSetup(): bool
+    {
+        // ... if the migrator itself needs setup.
+        if ($this->migrator->needsSetup()) {
+            return true;
+        }
+
+        // ... if JobsStore migrations need to run
+        if (!$this->areAllMigrationsImplemented()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function areAllMigrationsImplemented(): bool
+    {
+        return !$this->migrator->hasNonImplementedMigrationClasses(
+            $this->getMigrationsDirectory(),
+            $this->getMigrationsNamespace()
+        );
+    }
+
+    protected function getMigrationsDirectory(): string
+    {
+        return __DIR__ . DIRECTORY_SEPARATOR .
+            (new ReflectionClass($this))->getShortName() . DIRECTORY_SEPARATOR .
+            AbstractMigrator::DEFAULT_MIGRATIONS_DIRECTORY_NAME;
+    }
+
+    protected function getMigrationsNamespace(): string
     {
-        // TODO mivanci dovrši
-        return true;
+        return self::class . '\\' . AbstractMigrator::DEFAULT_MIGRATIONS_DIRECTORY_NAME;
     }
 
     public function getPrefixedTableNameJobs(): string
diff --git a/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000000CreateJobsTable.php b/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000000CreateJobsTable.php
index b0e7581ee8212e5717f74b09b337bfadc9f3e060..d807e673e5040ff7b150f7d0d6ed782c4f49f3bf 100644
--- a/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000000CreateJobsTable.php
+++ b/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000000CreateJobsTable.php
@@ -1,37 +1,59 @@
 <?php
 
+declare(strict_types=1);
+
 namespace SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations;
 
 use Doctrine\DBAL\Schema\Table;
 use Doctrine\DBAL\Types\Types;
+use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException;
 use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration;
 use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore;
+use Throwable;
 
 class Version20220601000000CreateJobsTable extends AbstractMigration
 {
+    /**
+     * @throws MigrationException
+     */
     public function run(): void
     {
         $tableName = $this->connection->preparePrefixedTableName(JobsStore::TABLE_NAME_JOBS);
-        $table = new Table($tableName);
 
-        $table->addColumn('id', Types::BIGINT)
-            ->setUnsigned(true)
-            ->setAutoincrement(true);
+        try {
+            $table = new Table($tableName);
+
+            $table->addColumn(JobsStore::COLUMN_NAME_ID, Types::BIGINT)
+                ->setUnsigned(true)
+                ->setAutoincrement(true);
+
+            $table->addColumn(JobsStore::COLUMN_NAME_TYPE, Types::STRING)
+                ->setLength(JobsStore::COLUMN_LENGTH_TYPE);
 
-        $table->addColumn('payload', Types::TEXT);
-        $table->addColumn('created_at', Types::DATETIMETZ_IMMUTABLE);
-        $table->addColumn('reserved_at', Types::DATETIMETZ_IMMUTABLE)->setNotnull(false);
-        $table->addColumn('attempts', Types::INTEGER)->setNotnull(false);
+            $table->addColumn(JobsStore::COLUMN_NAME_PAYLOAD, Types::TEXT);
+            $table->addColumn(JobsStore::COLUMN_NAME_CREATED_AT, Types::DATETIMETZ_IMMUTABLE);
 
-        $table->setPrimaryKey(['id']);
+            $table->setPrimaryKey([JobsStore::COLUMN_NAME_ID]);
 
-        $this->schemaManager->createTable($table);
+            $this->schemaManager->createTable($table);
+        } catch (Throwable $exception) {
+            $contextDetails = sprintf('Could not create table %s.', $tableName);
+            $this->throwGenericMigrationException($contextDetails, $exception);
+        }
     }
 
+    /**
+     * @throws MigrationException
+     */
     public function revert(): void
     {
         $tableName = $this->connection->preparePrefixedTableName(JobsStore::TABLE_NAME_JOBS);
 
-        $this->schemaManager->dropTable($tableName);
+        try {
+            $this->schemaManager->dropTable($tableName);
+        } catch (Throwable $exception) {
+            $contextDetails = sprintf('Could not drop table %s.', $tableName);
+            $this->throwGenericMigrationException($contextDetails, $exception);
+        }
     }
 }
diff --git a/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000100CreateFailedJobsTable.php b/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000100CreateFailedJobsTable.php
index e965025d763da8eda7fbc875b8bf7f9403080844..08a39ba94f094c879a8208211643757ab9df1acb 100644
--- a/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000100CreateFailedJobsTable.php
+++ b/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000100CreateFailedJobsTable.php
@@ -1,35 +1,55 @@
 <?php
 
+declare(strict_types=1);
+
 namespace SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations;
 
 use Doctrine\DBAL\Schema\Table;
 use Doctrine\DBAL\Types\Types;
+use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException;
 use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration;
 use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore;
+use Throwable;
 
 class Version20220601000100CreateFailedJobsTable extends AbstractMigration
 {
+    /**
+     * @throws MigrationException
+     */
     public function run(): void
     {
         $tableName = $this->connection->preparePrefixedTableName(JobsStore::TABLE_NAME_FAILED_JOBS);
-        $table = new Table($tableName);
+        try {
+            $table = new Table($tableName);
 
-        $table->addColumn('id', Types::BIGINT)
-            ->setUnsigned(true)
-            ->setAutoincrement(true);
+            $table->addColumn('id', Types::BIGINT)
+                ->setUnsigned(true)
+                ->setAutoincrement(true);
 
-        $table->addColumn('payload', Types::TEXT);
-        $table->addColumn('created_at', Types::DATETIMETZ_IMMUTABLE);
+            $table->addColumn('payload', Types::TEXT);
+            $table->addColumn('created_at', Types::DATETIMETZ_IMMUTABLE);
 
-        $table->setPrimaryKey(['id']);
+            $table->setPrimaryKey(['id']);
 
-        $this->schemaManager->createTable($table);
+            $this->schemaManager->createTable($table);
+        } catch (Throwable $exception) {
+            $contextDetails = sprintf('Could not create table %s.', $tableName);
+            $this->throwGenericMigrationException($contextDetails, $exception);
+        }
     }
 
+    /**
+     * @throws MigrationException
+     */
     public function revert(): void
     {
         $tableName = $this->connection->preparePrefixedTableName(JobsStore::TABLE_NAME_FAILED_JOBS);
 
-        $this->schemaManager->dropTable($tableName);
+        try {
+            $this->schemaManager->dropTable($tableName);
+        } catch (Throwable $exception) {
+            $contextDetails = sprintf('Could not drop table %s.', $tableName);
+            $this->throwGenericMigrationException($contextDetails, $exception);
+        }
     }
 }
diff --git a/src/Stores/Jobs/DoctrineDbal/RawJob.php b/src/Stores/Jobs/DoctrineDbal/RawJob.php
new file mode 100644
index 0000000000000000000000000000000000000000..e66eb263925311c1331a20894cf8e331d6f18542
--- /dev/null
+++ b/src/Stores/Jobs/DoctrineDbal/RawJob.php
@@ -0,0 +1,129 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal;
+
+use Doctrine\DBAL\Platforms\AbstractPlatform;
+use Doctrine\DBAL\Types\Type;
+use Doctrine\DBAL\Types\Types;
+use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload;
+use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException;
+
+class RawJob
+{
+    protected int $id;
+    protected AbstractPayload $payload;
+    protected string $type;
+    protected \DateTimeImmutable $createdAt;
+    protected AbstractPlatform $abstractPlatform;
+
+    public function __construct(array $rawRow, AbstractPlatform $abstractPlatform)
+    {
+        $this->abstractPlatform = $abstractPlatform;
+
+        $this->validate($rawRow);
+
+        $this->id = (int)$rawRow[JobsStore::COLUMN_NAME_ID];
+        $this->payload = $this->resolvePayload((string)$rawRow[JobsStore::COLUMN_NAME_PAYLOAD]);
+        $this->type = (string)$rawRow[JobsStore::COLUMN_NAME_TYPE];
+        $this->createdAt = $this->resolveCreatedAt($rawRow);
+    }
+
+    protected function validate(array $rawRow): void
+    {
+        $columnsToCheck = [
+            JobsStore::COLUMN_NAME_ID,
+            JobsStore::COLUMN_NAME_PAYLOAD,
+            JobsStore::COLUMN_NAME_TYPE,
+            JobsStore::COLUMN_NAME_CREATED_AT,
+        ];
+
+        foreach ($columnsToCheck as $column) {
+            if (empty($rawRow[$column])) {
+                throw new UnexpectedValueException(sprintf('Column %s must be set.', $column));
+            }
+        }
+
+        if (! is_numeric($rawRow[JobsStore::COLUMN_NAME_ID])) {
+            throw new UnexpectedValueException(sprintf('Column %s must be numeric.', JobsStore::COLUMN_NAME_ID));
+        }
+
+        if (! is_string($rawRow[JobsStore::COLUMN_NAME_PAYLOAD])) {
+            throw new UnexpectedValueException(sprintf('Column %s must be string.', JobsStore::COLUMN_NAME_PAYLOAD));
+        }
+
+        if (! is_string($rawRow[JobsStore::COLUMN_NAME_TYPE])) {
+            throw new UnexpectedValueException(sprintf('Column %s must be string.', JobsStore::COLUMN_NAME_TYPE));
+        }
+
+        if (! is_string($rawRow[JobsStore::COLUMN_NAME_CREATED_AT])) {
+            throw new UnexpectedValueException(sprintf('Column %s must be string.', JobsStore::COLUMN_NAME_CREATED_AT));
+        }
+    }
+
+    protected function resolvePayload(string $rawPayload): AbstractPayload
+    {
+        /** @psalm-suppress MixedAssignment - we check the type manually */
+        $payload = unserialize($rawPayload);
+
+        if ($payload instanceof AbstractPayload) {
+            return $payload;
+        }
+
+        throw new UnexpectedValueException('Job payload is not instance of AbstractPayload.');
+    }
+
+    /**
+     * @return int
+     */
+    public function getId(): int
+    {
+        return $this->id;
+    }
+
+    /**
+     * @return AbstractPayload
+     */
+    public function getPayload(): AbstractPayload
+    {
+        return $this->payload;
+    }
+
+    /**
+     * @return string
+     */
+    public function getType(): string
+    {
+        return $this->type;
+    }
+
+    protected function resolveCreatedAt(array $rawRow): \DateTimeImmutable
+    {
+        try {
+            /** @var \DateTimeImmutable $createdAt */
+            $createdAt = (Type::getType(Types::DATETIME_IMMUTABLE))
+                ->convertToPHPValue($rawRow[JobsStore::COLUMN_NAME_CREATED_AT], $this->abstractPlatform);
+        } catch (\Throwable $exception) {
+            throw new UnexpectedValueException(
+                sprintf(
+                    'Could not create instance of DateTimeImmutable using value %s for column %s.',
+                    var_export($rawRow[JobsStore::COLUMN_NAME_CREATED_AT], true),
+                    JobsStore::COLUMN_NAME_CREATED_AT
+                ),
+                (int)$exception->getCode(),
+                $exception
+            );
+        }
+
+        return $createdAt;
+    }
+
+    /**
+     * @return \DateTimeImmutable
+     */
+    public function getCreatedAt(): \DateTimeImmutable
+    {
+        return $this->createdAt;
+    }
+}
diff --git a/src/Stores/Jobs/Interfaces/JobsStoreInterface.php b/src/Stores/Jobs/Interfaces/JobsStoreInterface.php
deleted file mode 100644
index d7ef16b1d6a60b99087a02f5d0e208d2d6e39e05..0000000000000000000000000000000000000000
--- a/src/Stores/Jobs/Interfaces/JobsStoreInterface.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php
-
-namespace SimpleSAML\Module\accounting\Stores\Jobs\Interfaces;
-
-interface JobsStoreInterface
-{
-}
diff --git a/src/Stores/Jobs/Interfaces/JobsStoreMigrationInterface.php b/src/Stores/Jobs/Interfaces/JobsStoreMigrationInterface.php
deleted file mode 100644
index 9a935152ec7dcb368c10afbc01c092d46abb17c0..0000000000000000000000000000000000000000
--- a/src/Stores/Jobs/Interfaces/JobsStoreMigrationInterface.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php
-
-namespace SimpleSAML\Module\accounting\Stores\Jobs\Interfaces;
-
-interface JobsStoreMigrationInterface
-{
-    /**
-     * Run this jobs store migration.
-     * @return void
-     */
-    public function run(): void;
-}
diff --git a/tests/src/Entities/AuthenticationEvent/JobTest.php b/tests/src/Entities/AuthenticationEvent/JobTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..16824118e8d757a34b08f6e4c8502fa52e12e82f
--- /dev/null
+++ b/tests/src/Entities/AuthenticationEvent/JobTest.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace SimpleSAML\Test\Module\accounting\Entities\AuthenticationEvent;
+
+use SimpleSAML\Module\accounting\Entities\AuthenticationEvent;
+use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload;
+use SimpleSAML\Module\accounting\Entities\AuthenticationEvent\Job;
+use PHPUnit\Framework\TestCase;
+use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException;
+
+/**
+ * @covers \SimpleSAML\Module\accounting\Entities\AuthenticationEvent\Job
+ * @uses \SimpleSAML\Module\accounting\Entities\Bases\AbstractJob
+ * @uses \SimpleSAML\Module\accounting\Entities\AuthenticationEvent
+ */
+class JobTest extends TestCase
+{
+    public function testCanCreateInstanceWithAuthenticationEventEntity(): void
+    {
+        $job = new Job(new AuthenticationEvent(['sample' => 'state']));
+
+        $this->assertInstanceOf(AuthenticationEvent::class, $job->getPayload());
+    }
+
+    public function testPayloadMustBeAuthenticationEventOrThrow(): void
+    {
+        $payload = new class extends AbstractPayload {
+        };
+
+        $this->expectException(UnexpectedValueException::class);
+
+        (new Job($payload));
+    }
+}
diff --git a/tests/src/Entities/AuthenticationEventTest.php b/tests/src/Entities/AuthenticationEventTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..bf4ceb2101c946b9abea1fc5bbe8f662d5d88aa3
--- /dev/null
+++ b/tests/src/Entities/AuthenticationEventTest.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace SimpleSAML\Test\Module\accounting\Entities;
+
+use SimpleSAML\Module\accounting\Entities\AuthenticationEvent;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \SimpleSAML\Module\accounting\Entities\AuthenticationEvent
+ */
+class AuthenticationEventTest extends TestCase
+{
+    protected array $state = [
+        'sample' => 'value',
+    ];
+
+
+    public function testCanGetState(): void
+    {
+        $authenticationEvent = new AuthenticationEvent($this->state);
+
+        $this->assertArrayHasKey('sample', $authenticationEvent->getState());
+    }
+}
diff --git a/tests/src/Entities/Bases/AbstractJobTest.php b/tests/src/Entities/Bases/AbstractJobTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..86fa21be640475adb53764e39f71e74f8c8f6cf0
--- /dev/null
+++ b/tests/src/Entities/Bases/AbstractJobTest.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace SimpleSAML\Test\Module\accounting\Entities\Bases;
+
+use SimpleSAML\Module\accounting\Entities\AuthenticationEvent;
+use SimpleSAML\Module\accounting\Entities\AuthenticationEvent\Job;
+use SimpleSAML\Module\accounting\Entities\Bases\AbstractJob;
+use PHPUnit\Framework\TestCase;
+use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload;
+
+/**
+ * @covers \SimpleSAML\Module\accounting\Entities\Bases\AbstractJob
+ * @uses \SimpleSAML\Module\accounting\Entities\AuthenticationEvent\Job
+ * @uses \SimpleSAML\Module\accounting\Entities\AuthenticationEvent
+ */
+class AbstractJobTest extends TestCase
+{
+    protected AbstractPayload $payload;
+
+    protected function setUp(): void
+    {
+        $this->payload = new class extends AbstractPayload {
+        };
+    }
+
+    public function testCanSetAndGetPayload(): void
+    {
+        $job = new class ($this->payload) extends AbstractJob  {
+            public function run(): void
+            {
+            }
+        };
+
+        $this->assertInstanceOf(AbstractPayload::class, $job->getPayload());
+    }
+}
diff --git a/tests/src/Services/LoggerServiceTest.php b/tests/src/Services/LoggerServiceTest.php
index 364cdbf9b1b7c9dab5104bb354ca1647ed533751..a1ffe30c1dffb0b8199303755c8fa0d5e2c18fe2 100644
--- a/tests/src/Services/LoggerServiceTest.php
+++ b/tests/src/Services/LoggerServiceTest.php
@@ -24,6 +24,8 @@ class LoggerServiceTest extends TestCase
         $loggerService->critical('test');
         $loggerService->emergency('test');
 
+        $loggerService->emergency('test', ['sample' => 'context']);
+
         $this->assertTrue(true); // Nothing to evaluate
     }
 }
diff --git a/tests/src/Stores/Connections/Bases/AbstractMigratorTest.php b/tests/src/Stores/Connections/Bases/AbstractMigratorTest.php
index 1cd96d8b2a24b11a76e126a4f1dcf69c374f56cc..a9bda1bf9ed49eeb2630b7d6585f2fe7582a397c 100644
--- a/tests/src/Stores/Connections/Bases/AbstractMigratorTest.php
+++ b/tests/src/Stores/Connections/Bases/AbstractMigratorTest.php
@@ -3,7 +3,7 @@
 namespace SimpleSAML\Test\Module\accounting\Stores\Connections\Bases;
 
 use Doctrine\DBAL\Schema\AbstractSchemaManager;
-use SimpleSAML\Module\accounting\Exceptions\MigrationException;
+use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException;
 use SimpleSAML\Module\accounting\ModuleConfiguration;
 use SimpleSAML\Module\accounting\Services\LoggerService;
 use SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator;
@@ -160,7 +160,7 @@ class AbstractMigratorTest extends TestCase
 
     public function testCanRunNonImplementedMigrationClasses(): void
     {
-/** @psalm-suppress InvalidArgument Using mock instead of LoggerService instance */
+        /** @psalm-suppress InvalidArgument Using mock instead of LoggerService instance */
         $migrator = new Migrator($this->connection, $this->loggerServiceMock);
 
         $migrator->runSetup();
diff --git a/tests/src/Stores/Connections/DoctrineDbal/Bases/AbstractMigrationTest.php b/tests/src/Stores/Connections/DoctrineDbal/Bases/AbstractMigrationTest.php
index bdbef5430da50c68a810fef9a7413fb137d34a8d..954233cdd3778966802b1bb235dc86299a634e82 100644
--- a/tests/src/Stores/Connections/DoctrineDbal/Bases/AbstractMigrationTest.php
+++ b/tests/src/Stores/Connections/DoctrineDbal/Bases/AbstractMigrationTest.php
@@ -2,6 +2,7 @@
 
 namespace SimpleSAML\Test\Module\accounting\Stores\Connections\DoctrineDbal\Bases;
 
+use SimpleSAML\Module\accounting\Exceptions\StoreException;
 use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration;
 use PHPUnit\Framework\TestCase;
 use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection;
@@ -14,13 +15,50 @@ use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations\V
  */
 class AbstractMigrationTest extends TestCase
 {
-    public function testCanInstantiateMigrationClass(): void
+    protected Connection $connection;
+
+    protected function setUp(): void
     {
-        $connection = new Connection(['driver' => 'pdo_sqlite', 'memory' => true,]);
+        parent::setUp();
+        $this->connection = new Connection(['driver' => 'pdo_sqlite', 'memory' => true,]);
+    }
 
+    public function testCanInstantiateMigrationClass(): void
+    {
         $this->assertInstanceOf(
             AbstractMigration::class,
-            new Version20220601000000CreateJobsTable($connection)
+            new Version20220601000000CreateJobsTable($this->connection)
         );
     }
+
+    public function testThrowsStoreException(): void
+    {
+        $dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class);
+        $dbalStub->method('createSchemaManager')
+            ->willThrowException(new \Doctrine\DBAL\Exception('test'));
+        $connectionStub = $this->createStub(Connection::class);
+        $connectionStub->method('dbal')->willReturn($dbalStub);
+
+        $this->expectException(StoreException::class);
+
+        (new Version20220601000000CreateJobsTable($connectionStub));
+    }
+
+    public function testCanThrowGenericMigrationExceptionOnRun(): void
+    {
+        $migration = new class ($this->connection) extends AbstractMigration {
+            public function run(): void
+            {
+                $this->throwGenericMigrationException('test', new \Exception('test'));
+            }
+
+            public function revert(): void
+            {
+            }
+        };
+
+        $this->expectException(StoreException\MigrationException::class);
+
+        $migration->run();
+    }
 }
diff --git a/tests/src/Stores/Connections/DoctrineDbal/MigratorTest.php b/tests/src/Stores/Connections/DoctrineDbal/MigratorTest.php
index f02cd4b554f61a04e27c22b43651fca9c3f3f877..bfa5093c4f3af1461a29695ff47b467e54dcfdf8 100644
--- a/tests/src/Stores/Connections/DoctrineDbal/MigratorTest.php
+++ b/tests/src/Stores/Connections/DoctrineDbal/MigratorTest.php
@@ -2,9 +2,11 @@
 
 namespace SimpleSAML\Test\Module\accounting\Stores\Connections\DoctrineDbal;
 
+use Doctrine\DBAL\Query\QueryBuilder;
 use PHPUnit\Framework\TestCase;
 use Doctrine\DBAL\Schema\AbstractSchemaManager;
 use SimpleSAML\Module\accounting\Exceptions\InvalidValueException;
+use SimpleSAML\Module\accounting\Exceptions\StoreException;
 use SimpleSAML\Module\accounting\ModuleConfiguration;
 use SimpleSAML\Module\accounting\Services\LoggerService;
 use SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator;
@@ -75,7 +77,7 @@ class MigratorTest extends TestCase
         $this->assertTrue($this->schemaManager->tablesExist([$this->tableName]));
     }
 
-    public function testRunningMigratiorSetupMultipleTimesLogsWarning(): void
+    public function testRunningMigratorSetupMultipleTimesLogsWarning(): void
     {
         $this->loggerServiceMock
             ->expects($this->once())
@@ -144,6 +146,90 @@ class MigratorTest extends TestCase
         $this->assertNotEmpty($migrator->getImplementedMigrationClasses());
     }
 
+    public function testThrowsStoreExceptionOnInitialization(): void
+    {
+        $dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class);
+        $dbalStub->method('createSchemaManager')->willThrowException(new \Doctrine\DBAL\Exception('test'));
+        $connectionStub = $this->createStub(Connection::class);
+        $connectionStub->method('dbal')->willReturn($dbalStub);
+
+        $this->expectException(StoreException::class);
+
+        /** @psalm-suppress InvalidArgument */
+        (new Migrator($connectionStub, $this->loggerServiceMock));
+    }
+
+    public function testThrowsStoreExceptionOnNeedsSetup(): void
+    {
+        $schemaManagerStub = $this->createStub(AbstractSchemaManager::class);
+        $schemaManagerStub->method('tablesExist')
+            ->willThrowException(new \Doctrine\DBAL\Exception('test'));
+        $dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class);
+        $dbalStub->method('createSchemaManager')->willReturn($schemaManagerStub);
+        $connectionStub = $this->createStub(Connection::class);
+        $connectionStub->method('dbal')->willReturn($dbalStub);
+
+        /** @psalm-suppress InvalidArgument */
+        $migrator = new Migrator($connectionStub, $this->loggerServiceMock);
+
+        $this->expectException(StoreException::class);
+
+        $migrator->needsSetup();
+    }
+
+    public function testThrowsStoreExceptionOnCreateMigrationsTable(): void
+    {
+        $schemaManagerStub = $this->createStub(AbstractSchemaManager::class);
+        $schemaManagerStub->method('tablesExist')
+            ->willReturn(false);
+        $dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class);
+        $dbalStub->method('createSchemaManager')->willReturn($schemaManagerStub);
+        $connectionStub = $this->createStub(Connection::class);
+        $connectionStub->method('dbal')->willReturn($dbalStub);
+
+        /** @psalm-suppress InvalidArgument */
+        $migrator = new Migrator($connectionStub, $this->loggerServiceMock);
+
+        $this->expectException(StoreException::class);
+
+        $migrator->runSetup();
+    }
+
+    public function testThrowsStoreExceptionOnMarkingImplementedClass(): void
+    {
+        $queryBuilderStub = $this->createStub(QueryBuilder::class);
+        $queryBuilderStub->method('insert')
+            ->willThrowException(new \Doctrine\DBAL\Exception('test'));
+        $dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class);
+        $dbalStub->method('createQueryBuilder')->willReturn($queryBuilderStub);
+        $connectionStub = $this->createStub(Connection::class);
+        $connectionStub->method('dbal')->willReturn($dbalStub);
+        $connectionStub->method('preparePrefixedTableName')->willReturn(Migrator::TABLE_NAME);
+
+        /** @psalm-suppress InvalidArgument */
+        $migrator = new Migrator($connectionStub, $this->loggerServiceMock);
+        $migrator->runSetup();
+
+        $this->expectException(StoreException::class);
+
+        $migrator->runMigrationClasses([JobsStore\Migrations\Version20220601000000CreateJobsTable::class]);
+    }
+
+    public function testThrowsStoreExceptionOnGetImplementedMigrationClasses(): void
+    {
+        $schemaManagerStub = $this->createStub(AbstractSchemaManager::class);
+        $dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class);
+        $dbalStub->method('createQueryBuilder')->willThrowException(new \Doctrine\DBAL\Exception('test'));
+        $dbalStub->method('createSchemaManager')->willReturn($schemaManagerStub);
+        $connectionStub = $this->createStub(Connection::class);
+        $connectionStub->method('dbal')->willReturn($dbalStub);
+
+        $this->expectException(StoreException::class);
+
+        /** @psalm-suppress InvalidArgument */
+        (new Migrator($connectionStub, $this->loggerServiceMock))->getImplementedMigrationClasses();
+    }
+
     protected function getSampleMigrationsDirectory(): string
     {
         return $this->moduleConfiguration->getModuleSourceDirectory() . DIRECTORY_SEPARATOR .
diff --git a/tests/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000000CreateJobsTableTest.php b/tests/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000000CreateJobsTableTest.php
index ad2b7e99a145e3d088e2c3c5502f0e468bb7e618..011b249112ee52f246d272fed33acc107639241d 100644
--- a/tests/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000000CreateJobsTableTest.php
+++ b/tests/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000000CreateJobsTableTest.php
@@ -2,6 +2,8 @@
 
 namespace SimpleSAML\Test\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations;
 
+use Doctrine\DBAL\Schema\AbstractSchemaManager;
+use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException;
 use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection;
 use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore;
 use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations\Version20220601000000CreateJobsTable;
@@ -34,4 +36,36 @@ class Version20220601000000CreateJobsTableTest extends TestCase
         $migration->revert();
         $this->assertFalse($this->schemaManager->tablesExist($this->tableName));
     }
+
+    public function testRunThrowsMigrationException(): void
+    {
+        $connectionStub = $this->createStub(Connection::class);
+        $dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class);
+        $schemaManagerStub = $this->createStub(AbstractSchemaManager::class);
+
+        $connectionStub->method('dbal')->willReturn($dbalStub);
+        $dbalStub->method('createSchemaManager')->willReturn($schemaManagerStub);
+        $schemaManagerStub->method('createTable')
+            ->willThrowException(new \Doctrine\DBAL\Exception('test'));
+
+        $migration = new Version20220601000000CreateJobsTable($connectionStub);
+        $this->expectException(MigrationException::class);
+        $migration->run();
+    }
+
+    public function testRevertThrowsMigrationException(): void
+    {
+        $connectionStub = $this->createStub(Connection::class);
+        $dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class);
+        $schemaManagerStub = $this->createStub(AbstractSchemaManager::class);
+
+        $connectionStub->method('dbal')->willReturn($dbalStub);
+        $dbalStub->method('createSchemaManager')->willReturn($schemaManagerStub);
+        $schemaManagerStub->method('dropTable')
+            ->willThrowException(new \Doctrine\DBAL\Exception('test'));
+
+        $migration = new Version20220601000000CreateJobsTable($connectionStub);
+        $this->expectException(MigrationException::class);
+        $migration->revert();
+    }
 }
diff --git a/tests/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000100CreateFailedJobsTableTest.php b/tests/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000100CreateFailedJobsTableTest.php
index c99092eef1c563ff1ac8d685ac52256866616cea..d6624b3f291282e481153e8cd1f7d413d25802ff 100644
--- a/tests/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000100CreateFailedJobsTableTest.php
+++ b/tests/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000100CreateFailedJobsTableTest.php
@@ -2,6 +2,8 @@
 
 namespace SimpleSAML\Test\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations;
 
+use Doctrine\DBAL\Schema\AbstractSchemaManager;
+use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException;
 use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection;
 use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore;
 use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations;
@@ -35,4 +37,36 @@ class Version20220601000100CreateFailedJobsTableTest extends TestCase
         $migration->revert();
         $this->assertFalse($this->schemaManager->tablesExist($this->tableName));
     }
+
+    public function testRunThrowsMigrationException(): void
+    {
+        $connectionStub = $this->createStub(Connection::class);
+        $dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class);
+        $schemaManagerStub = $this->createStub(AbstractSchemaManager::class);
+
+        $connectionStub->method('dbal')->willReturn($dbalStub);
+        $dbalStub->method('createSchemaManager')->willReturn($schemaManagerStub);
+        $schemaManagerStub->method('createTable')
+            ->willThrowException(new \Doctrine\DBAL\Exception('test'));
+
+        $migration = new Migrations\Version20220601000100CreateFailedJobsTable($connectionStub);
+        $this->expectException(MigrationException::class);
+        $migration->run();
+    }
+
+    public function testRevertThrowsMigrationException(): void
+    {
+        $connectionStub = $this->createStub(Connection::class);
+        $dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class);
+        $schemaManagerStub = $this->createStub(AbstractSchemaManager::class);
+
+        $connectionStub->method('dbal')->willReturn($dbalStub);
+        $dbalStub->method('createSchemaManager')->willReturn($schemaManagerStub);
+        $schemaManagerStub->method('dropTable')
+            ->willThrowException(new \Doctrine\DBAL\Exception('test'));
+
+        $migration = new Migrations\Version20220601000100CreateFailedJobsTable($connectionStub);
+        $this->expectException(MigrationException::class);
+        $migration->revert();
+    }
 }
diff --git a/tests/src/Stores/Jobs/DoctrineDbal/JobsStoreTest.php b/tests/src/Stores/Jobs/DoctrineDbal/JobsStoreTest.php
index cbb4013c7b590a7a8120deae3c2c9185c27191d7..3686a6f4acf623a132a381dcec22247e86dc9caa 100644
--- a/tests/src/Stores/Jobs/DoctrineDbal/JobsStoreTest.php
+++ b/tests/src/Stores/Jobs/DoctrineDbal/JobsStoreTest.php
@@ -2,11 +2,16 @@
 
 namespace SimpleSAML\Test\Module\accounting\Stores\Jobs\DoctrineDbal;
 
-use PHPUnit\Framework\MockObject\MockObject;
+use SimpleSAML\Module\accounting\Entities\AuthenticationEvent;
+use SimpleSAML\Module\accounting\Entities\Bases\AbstractJob;
+use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload;
+use SimpleSAML\Module\accounting\Entities\Interfaces\JobInterface;
+use SimpleSAML\Module\accounting\Exceptions\StoreException;
 use SimpleSAML\Module\accounting\ModuleConfiguration;
 use SimpleSAML\Module\accounting\Services\LoggerService;
 use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection;
 use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Factory;
+use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator;
 use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore;
 use PHPUnit\Framework\TestCase;
 
@@ -15,26 +20,73 @@ use PHPUnit\Framework\TestCase;
  * @uses \SimpleSAML\Module\accounting\ModuleConfiguration
  * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection
  * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Factory
+ * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator
+ * @uses \SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator
+ * @uses \SimpleSAML\Module\accounting\Helpers\FilesystemHelper
+ * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration
+ * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations\Version20220601000000CreateJobsTable
+ * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations\Version20220601000100CreateFailedJobsTable
+ * @uses \SimpleSAML\Module\accounting\Entities\Bases\AbstractJob
+ * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\RawJob
+ * @uses \SimpleSAML\Module\accounting\Entities\AuthenticationEvent
+ * @uses \SimpleSAML\Module\accounting\Entities\AuthenticationEvent\Job
  */
 class JobsStoreTest extends TestCase
 {
     protected ModuleConfiguration $moduleConfiguration;
-    protected \PHPUnit\Framework\MockObject\Stub $factory;
+    protected \PHPUnit\Framework\MockObject\Stub $factoryStub;
     protected Connection $connection;
+    protected \PHPUnit\Framework\MockObject\Stub $loggerServiceStub;
+    protected Migrator $migrator;
 
     protected function setUp(): void
     {
         // Configuration directory is set by phpunit using php ENV setting feature (check phpunit.xml).
         $this->moduleConfiguration = new ModuleConfiguration('module_accounting.php');
         $this->connection = new Connection(['driver' => 'pdo_sqlite', 'memory' => true,]);
-        $this->factory = $this->createStub(Factory::class);
-        $this->factory->method('buildConnection')->willReturn($this->connection);
+
+        $this->loggerServiceStub = $this->createStub(LoggerService::class);
+
+        /** @psalm-suppress InvalidArgument */
+        $this->migrator = new Migrator($this->connection, $this->loggerServiceStub);
+
+        $this->factoryStub = $this->createStub(Factory::class);
+        $this->factoryStub->method('buildConnection')->willReturn($this->connection);
+        $this->factoryStub->method('buildMigrator')->willReturn($this->migrator);
+    }
+
+    public function testSetupDependsOnMigratorSetup(): void
+    {
+        /** @psalm-suppress InvalidArgument */
+        $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub);
+
+        $this->assertTrue($this->migrator->needsSetup());
+        $this->assertTrue($jobsStore->needsSetup());
+
+        $jobsStore->runSetup();
+
+        $this->assertFalse($jobsStore->needsSetup());
+        $this->assertFalse($this->migrator->needsSetup());
+    }
+
+    public function testSetupDependsOnMigrations(): void
+    {
+        /** @psalm-suppress InvalidArgument */
+        $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub);
+
+        // Run migrator setup beforehand, so it only depends on JobsStore migrations setup
+        $this->migrator->runSetup();
+        $this->assertTrue($jobsStore->needsSetup());
+
+        $jobsStore->runSetup();
+
+        $this->assertFalse($jobsStore->needsSetup());
     }
 
     public function testCanGetPrefixedTableNames(): void
     {
         /** @psalm-suppress InvalidArgument */
-        $jobsStore = new JobsStore($this->moduleConfiguration, $this->factory);
+        $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub);
 
         $tableNameJobs = $this->connection->preparePrefixedTableName(JobsStore::TABLE_NAME_JOBS);
         $tableNameFailedJobs = $this->connection->preparePrefixedTableName(JobsStore::TABLE_NAME_FAILED_JOBS);
@@ -42,4 +94,181 @@ class JobsStoreTest extends TestCase
         $this->assertSame($tableNameJobs, $jobsStore->getPrefixedTableNameJobs());
         $this->assertSame($tableNameFailedJobs, $jobsStore->getPrefixedTableNameFailedJobs());
     }
+
+    public function testCanEnqueueJob(): void
+    {
+        /** @psalm-suppress InvalidArgument */
+        $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub);
+        $jobsStore->runSetup();
+
+        $payloadStub = $this->createStub(AbstractPayload::class);
+        $jobStub = $this->createStub(AbstractJob::class);
+        $jobStub->method('getPayload')->willReturn($payloadStub);
+
+        $queryBuilder = $this->connection->dbal()->createQueryBuilder();
+        $queryBuilder->select('COUNT(id) as jobsCount')->from($jobsStore->getPrefixedTableNameJobs())->fetchOne();
+
+        $this->assertSame(0, (int) $queryBuilder->executeQuery()->fetchOne());
+
+        $jobsStore->enqueue($jobStub);
+
+        $this->assertSame(1, (int) $queryBuilder->executeQuery()->fetchOne());
+
+        $jobsStore->enqueue($jobStub);
+        $jobsStore->enqueue($jobStub);
+
+        $this->assertSame(3, (int) $queryBuilder->executeQuery()->fetchOne());
+    }
+
+    public function testEnqueueThrowsStoreExceptionOnNonSetupRun(): void
+    {
+        /** @psalm-suppress InvalidArgument */
+        $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub);
+        // Don't run setup, so we get exception
+        //$jobsStore->runSetup();
+
+        $payloadStub = $this->createStub(AbstractPayload::class);
+        $jobStub = $this->createStub(AbstractJob::class);
+        $jobStub->method('getPayload')->willReturn($payloadStub);
+
+        $this->expectException(StoreException::class);
+
+        $jobsStore->enqueue($jobStub);
+    }
+
+    public function testEnqueueThrowsStoreExceptionOnInvalidType(): void
+    {
+        /** @psalm-suppress InvalidArgument */
+        $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub);
+        // Don't run setup, so we get exception
+        $jobsStore->runSetup();
+
+        $payloadStub = $this->createStub(AbstractPayload::class);
+        $jobStub = $this->createStub(AbstractJob::class);
+        $jobStub->method('getPayload')->willReturn($payloadStub);
+        $invalidType = str_pad('invalid-type', JobsStore::COLUMN_LENGTH_TYPE + 1);
+
+        $this->expectException(StoreException::class);
+
+        $jobsStore->enqueue($jobStub, $invalidType);
+    }
+
+    public function testCanGetNextJob(): void
+    {
+        /** @psalm-suppress InvalidArgument */
+        $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub);
+        $jobsStore->runSetup();
+
+        $payloadStub = $this->createStub(AbstractPayload::class);
+        $jobStub = $this->createStub(AbstractJob::class);
+        $jobStub->method('getPayload')->willReturn($payloadStub);
+
+        $queryBuilder = $this->connection->dbal()->createQueryBuilder();
+        $queryBuilder->select('COUNT(id) as jobsCount')->from($jobsStore->getPrefixedTableNameJobs())->fetchOne();
+
+        $this->assertSame(0, (int) $queryBuilder->executeQuery()->fetchOne());
+
+        $jobsStore->enqueue($jobStub);
+
+        $this->assertNotEmpty($jobsStore->getNext()->fetchOne());
+        $this->assertNotEmpty($jobsStore->getNext(get_class($jobStub))->fetchOne());
+
+
+        $jobsStore->enqueue($jobStub, 'sample-type');
+        $this->assertNotEmpty($jobsStore->getNext('sample-type')->fetchOne());
+    }
+
+    public function testGetNextThrowsOnNonSetupRun(): void
+    {
+        /** @psalm-suppress InvalidArgument */
+        $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub);
+        // Don't run setup, so we get exception
+        //$jobsStore->runSetup();
+
+        $payloadStub = $this->createStub(AbstractPayload::class);
+        $jobStub = $this->createStub(AbstractJob::class);
+        $jobStub->method('getPayload')->willReturn($payloadStub);
+
+        $this->expectException(StoreException::class);
+
+        $jobsStore->getNext();
+    }
+
+    public function testCanDequeueJob(): void
+    {
+        /** @psalm-suppress InvalidArgument */
+        $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub);
+        $jobsStore->runSetup();
+
+        $payloadStub = $this->createStub(AbstractPayload::class);
+        $jobStub = $this->createStub(AbstractJob::class);
+        $jobStub->method('getPayload')->willReturn($payloadStub);
+
+        $queryBuilder = $this->connection->dbal()->createQueryBuilder();
+        $queryBuilder->select('COUNT(id) as jobsCount')->from($jobsStore->getPrefixedTableNameJobs())->fetchOne();
+
+        $this->assertSame(0, (int) $queryBuilder->executeQuery()->fetchOne());
+
+        $jobsStore->enqueue($jobStub);
+        $jobsStore->enqueue($jobStub);
+
+        $this->assertSame(2, (int) $queryBuilder->executeQuery()->fetchOne());
+
+        $jobsStore->dequeue();
+
+        $this->assertSame(1, (int) $queryBuilder->executeQuery()->fetchOne());
+    }
+
+    public function testCanDequeueSpecificJobType(): void
+    {
+        /** @psalm-suppress InvalidArgument */
+        $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub);
+        $jobsStore->runSetup();
+
+        $payloadStub = $this->createStub(AbstractPayload::class);
+        $jobStub = $this->createStub(AbstractJob::class);
+        $jobStub->method('getPayload')->willReturn($payloadStub);
+
+        $authenticationEvent = new AuthenticationEvent(['sample-state']);
+        $authenticationEventJob = new AuthenticationEvent\Job($authenticationEvent);
+
+        $queryBuilder = $this->connection->dbal()->createQueryBuilder();
+        $queryBuilder->select('COUNT(id) as jobsCount')->from($jobsStore->getPrefixedTableNameJobs())->fetchOne();
+
+        $this->assertSame(0, (int) $queryBuilder->executeQuery()->fetchOne());
+
+        $jobsStore->enqueue($jobStub, 'test-type');
+        $jobsStore->enqueue($authenticationEventJob);
+
+        $this->assertSame(2, (int) $queryBuilder->executeQuery()->fetchOne());
+
+        $this->assertInstanceOf(AuthenticationEvent\Job::class, $jobsStore->dequeue(AuthenticationEvent\Job::class));
+
+        $this->assertSame(1, (int) $queryBuilder->executeQuery()->fetchOne());
+
+        $this->assertNull($jobsStore->dequeue(AuthenticationEvent::class));
+
+        $this->assertSame(1, (int) $queryBuilder->executeQuery()->fetchOne());
+    }
+
+    public function testDequeueThrowsWhenSetupNotRun(): void
+    {
+        /** @psalm-suppress InvalidArgument */
+        $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub);
+//        $jobsStore->runSetup();
+
+        $payloadStub = $this->createStub(AbstractPayload::class);
+        $jobStub = $this->createStub(AbstractJob::class);
+        $jobStub->method('getPayload')->willReturn($payloadStub);
+
+        $this->expectException(StoreException::class);
+
+        $jobsStore->dequeue('test-type');
+    }
+
+    public function testCanContinueSearchingInCaseOfJobDeletion(): void
+    {
+        // TODO try to simulate job deletion
+        $this->markTestIncomplete();
+    }
 }
diff --git a/tests/src/Stores/Jobs/DoctrineDbal/RawJobTest.php b/tests/src/Stores/Jobs/DoctrineDbal/RawJobTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..6d9fe229e710b367db088a8e3aa071d77b56ee9b
--- /dev/null
+++ b/tests/src/Stores/Jobs/DoctrineDbal/RawJobTest.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace SimpleSAML\Test\Module\accounting\Stores\Jobs\DoctrineDbal;
+
+use Doctrine\DBAL\Platforms\AbstractPlatform;
+use Doctrine\DBAL\Platforms\SqlitePlatform;
+use SimpleSAML\Module\accounting\Entities\AuthenticationEvent;
+use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException;
+use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore;
+use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\RawJob;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\RawJob
+ * @uses \SimpleSAML\Module\accounting\Entities\AuthenticationEvent
+ */
+class RawJobTest extends TestCase
+{
+    protected AuthenticationEvent $authenticationEvent;
+    protected array $validRawRow;
+    protected \PHPUnit\Framework\MockObject\Stub $abstractPlatformStub;
+
+    protected function setUp(): void
+    {
+        $this->abstractPlatformStub = $this->createStub(AbstractPlatform::class);
+        $this->authenticationEvent = new AuthenticationEvent(['sample' => 'state']);
+        $this->validRawRow = [
+            JobsStore::COLUMN_NAME_ID => 1,
+            JobsStore::COLUMN_NAME_PAYLOAD => serialize($this->authenticationEvent),
+            JobsStore::COLUMN_NAME_TYPE => get_class($this->authenticationEvent),
+            JobsStore::COLUMN_NAME_CREATED_AT => '2022-08-17 13:26:12',
+        ];
+    }
+
+    public function testCanInstantiateValidRawJob(): void
+    {
+        $abstractPlatform = new SqlitePlatform();
+        $rawJob = new RawJob($this->validRawRow, $abstractPlatform);
+        $this->assertSame($rawJob->getId(), $this->validRawRow[JobsStore::COLUMN_NAME_ID]);
+        $this->assertEquals($rawJob->getPayload(), $this->authenticationEvent);
+        $this->assertSame($rawJob->getType(), $this->validRawRow[JobsStore::COLUMN_NAME_TYPE]);
+        $this->assertInstanceOf(\DateTimeImmutable::class, $rawJob->getCreatedAt());
+    }
+
+    public function testThrowsOnEmptyColumn(): void
+    {
+        $invalidRawRow = $this->validRawRow;
+        unset($invalidRawRow[JobsStore::COLUMN_NAME_ID]);
+
+        $this->expectException(UnexpectedValueException::class);
+
+        /** @psalm-suppress InvalidArgument */
+        new RawJob($invalidRawRow, $this->abstractPlatformStub);
+    }
+
+    public function testThrowsOnNonNumericId(): void
+    {
+        $invalidRawRow = $this->validRawRow;
+        $invalidRawRow[JobsStore::COLUMN_NAME_ID] = 'a';
+
+        $this->expectException(UnexpectedValueException::class);
+
+        /** @psalm-suppress InvalidArgument */
+        new RawJob($invalidRawRow, $this->abstractPlatformStub);
+    }
+
+    public function testThrowsOnNonStringPayload(): void
+    {
+        $invalidRawRow = $this->validRawRow;
+        $invalidRawRow[JobsStore::COLUMN_NAME_PAYLOAD] = 123;
+
+        $this->expectException(UnexpectedValueException::class);
+
+        /** @psalm-suppress InvalidArgument */
+        new RawJob($invalidRawRow, $this->abstractPlatformStub);
+    }
+
+    public function testThrowsOnNonAbstractPayload(): void
+    {
+        $invalidRawRow = $this->validRawRow;
+        $invalidRawRow[JobsStore::COLUMN_NAME_PAYLOAD] = serialize('abc');
+
+        $this->expectException(UnexpectedValueException::class);
+
+        /** @psalm-suppress InvalidArgument */
+        new RawJob($invalidRawRow, $this->abstractPlatformStub);
+    }
+
+    public function testThrowsOnNonStringType(): void
+    {
+        $invalidRawRow = $this->validRawRow;
+        $invalidRawRow[JobsStore::COLUMN_NAME_TYPE] = 123;
+
+        $this->expectException(UnexpectedValueException::class);
+
+        /** @psalm-suppress InvalidArgument */
+        new RawJob($invalidRawRow, $this->abstractPlatformStub);
+    }
+
+    public function testThrowsOnNonStringCreatedAt(): void
+    {
+        $invalidRawRow = $this->validRawRow;
+        $invalidRawRow[JobsStore::COLUMN_NAME_CREATED_AT] = 123;
+
+        $this->expectException(UnexpectedValueException::class);
+
+        /** @psalm-suppress InvalidArgument */
+        new RawJob($invalidRawRow, $this->abstractPlatformStub);
+    }
+
+    public function testThrowsOnNonValidCreatedAt(): void
+    {
+        $invalidRawRow = $this->validRawRow;
+        $invalidRawRow[JobsStore::COLUMN_NAME_CREATED_AT] = '123';
+
+        $this->expectException(UnexpectedValueException::class);
+
+        /** @psalm-suppress InvalidArgument */
+        new RawJob($invalidRawRow, $this->abstractPlatformStub);
+    }
+}