From f31debfbdcf115eaad4c605e05a9b83e97d93a45 Mon Sep 17 00:00:00 2001
From: Wolfgang Silbermayr <w.silbermayr@opentalk.eu>
Date: Fri, 7 Mar 2025 21:47:26 +0100
Subject: [PATCH] feat(training_participation_report): add configuration to api

Closes #972
---
 Cargo.lock                                    |  27 +--
 Cargo.toml                                    |   6 +-
 api/controller/frontend_api.yaml              | 103 +++++++++
 .../src/api/v1/events/instances.rs            |  46 +++-
 .../src/api/v1/events/invites.rs              |  24 +-
 .../src/api/v1/events/mod.rs                  | 157 ++++++++++++-
 .../events/shared_folder.rs                   |  24 +-
 .../src/events/notifications.rs               |  12 +-
 crates/opentalk-db-storage/src/events/mod.rs  | 211 +++++++++++++++---
 ...ng_participation_report_parameter_sets.sql |   7 +
 crates/opentalk-db-storage/src/schema.rs      |  14 ++
 .../src/lib.rs                                |  16 +-
 .../src/storage/mod.rs                        |   6 +-
 .../src/storage/redis.rs                      |   8 +-
 .../src/storage/room_state.rs                 |   3 +-
 .../training_participation_report_storage.rs  |   8 +-
 .../src/storage/volatile/memory.rs            |   8 +-
 .../src/storage/volatile/storage.rs           |   8 +-
 deny.toml                                     |   1 -
 docs/developer/database.md                    |   8 +
 20 files changed, 594 insertions(+), 103 deletions(-)
 create mode 100644 crates/opentalk-db-storage/src/migrations/V47__create_event_training_participation_report_parameter_sets.sql

diff --git a/Cargo.lock b/Cargo.lock
index b2e3fa04f..868a1db44 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1227,7 +1227,7 @@ dependencies = [
  "bitflags 2.9.0",
  "cexpr",
  "clang-sys",
- "itertools 0.12.1",
+ "itertools 0.11.0",
  "lazy_static",
  "lazycell",
  "log",
@@ -4371,15 +4371,6 @@ dependencies = [
  "either",
 ]
 
-[[package]]
-name = "itertools"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
-dependencies = [
- "either",
-]
-
 [[package]]
 name = "itertools"
 version = "0.14.0"
@@ -6088,9 +6079,9 @@ dependencies = [
 
 [[package]]
 name = "opentalk-types-api-v1"
-version = "0.33.0"
+version = "0.34.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8565d68e6994ab6d0d6b5bdbc37d94c4edff34900215309c88d0fe3e248a5924"
+checksum = "a7ffb9ca739d7b748de0e188c7a44f5fed08ecdd2d7063ac383e2c92eb2ba527"
 dependencies = [
  "actix-web",
  "base64 0.22.1",
@@ -6111,9 +6102,9 @@ dependencies = [
 
 [[package]]
 name = "opentalk-types-common"
-version = "0.32.0"
+version = "0.32.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebc7b6f5082ce4410a8a4b483a33ac523863a974344be8ffeeb339e37624a9fd"
+checksum = "13523221c1cf75be9cf89b5149542013472d58eafdadb085f6bdd93def143921"
 dependencies = [
  "actix-http",
  "actix-web-httpauth",
@@ -6365,9 +6356,9 @@ dependencies = [
 
 [[package]]
 name = "opentalk-types-signaling-training-participation-report"
-version = "0.2.0"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8658e3a6b4303b0d3f637d152029f3ee206ff1bdfdd411bd680094c15f9b004"
+checksum = "94dc459b07a8f2f60c2bd7c8c5713b38b053a9527e3995dee7c7ccd904040c4e"
 dependencies = [
  "opentalk-types-common",
  "opentalk-types-signaling",
@@ -7176,7 +7167,7 @@ checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4"
 dependencies = [
  "bytes",
  "heck 0.5.0",
- "itertools 0.12.1",
+ "itertools 0.11.0",
  "log",
  "multimap",
  "once_cell",
@@ -7216,7 +7207,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1"
 dependencies = [
  "anyhow",
- "itertools 0.12.1",
+ "itertools 0.11.0",
  "proc-macro2",
  "quote",
  "syn",
diff --git a/Cargo.toml b/Cargo.toml
index 06522e964..f008a7880 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -93,8 +93,8 @@ opentalk-mail-worker-protocol = "0.32"
 opentalk-nextcloud-client = { version = "0.2.0", default-features = false, features = [
   "rustls-tls-native-roots",
 ] }
-opentalk-types-api-v1 = "0.33"
-opentalk-types-common = "0.32"
+opentalk-types-api-v1 = "0.34"
+opentalk-types-common = "0.32.1"
 opentalk-types-signaling = "0.32"
 opentalk-types-signaling-breakout = "0.32"
 opentalk-types-signaling-chat = "0.32"
@@ -110,7 +110,7 @@ opentalk-types-signaling-recording-service = "0.32"
 opentalk-types-signaling-shared-folder = "0.32"
 opentalk-types-signaling-subroom-audio = "0.32"
 opentalk-types-signaling-timer = "0.32"
-opentalk-types-signaling-training-participation-report = "0.2"
+opentalk-types-signaling-training-participation-report = "0.3"
 opentalk-types-signaling-whiteboard = "0.32"
 opentalk-version = "0.1.1"
 opentelemetry = { version = "0.27", default-features = false, features = [
diff --git a/api/controller/frontend_api.yaml b/api/controller/frontend_api.yaml
index ea75ced0b..ac7db52cf 100644
--- a/api/controller/frontend_api.yaml
+++ b/api/controller/frontend_api.yaml
@@ -3095,6 +3095,13 @@ components:
         title:
           $ref: "#/components/schemas/EventTitle"
           description: Title of the event
+        training_participation_report:
+          $ref: "#/components/schemas/TrainingParticipationReportParameterSet"
+          description: |-
+            The training participation report parameter set for the event.
+
+            When present, the training participation report will be started
+            automatically in the meeting.
         type:
           $ref: "#/components/schemas/EventType"
           description: "Must always be `instance`"
@@ -3158,6 +3165,13 @@ components:
           timezone: Europe/Berlin
         status: ok
         title: Team Event
+        training_participation_report:
+          checkpoint_interval:
+            after: 300
+            within: 400
+          initial_checkpoint_delay:
+            after: 100
+            within: 200
         type: recurring
         updated_at: "2024-07-20T14:16:19Z"
         updated_by:
@@ -3315,6 +3329,13 @@ components:
             streaming_endpoint: "https://ingress.streaming.example.com/"
             streaming_key: aabbccddeeff
         title: Team Event
+        training_participation_report:
+          checkpoint_interval:
+            after: 300
+            within: 400
+          initial_checkpoint_delay:
+            after: 100
+            within: 200
         type: single
         updated_at: "2024-07-20T14:16:19Z"
         updated_by:
@@ -3453,6 +3474,13 @@ components:
             Title of the event
 
             For display purposes
+        training_participation_report:
+          $ref: "#/components/schemas/TrainingParticipationReportParameterSet"
+          description: |-
+            The training participation report parameter set for the event.
+
+            When present, the training participation report will be started
+            automatically in the meeting.
         type:
           $ref: "#/components/schemas/EventType"
           description: |-
@@ -3520,6 +3548,13 @@ components:
             streaming_endpoint: "https://ingress.streaming.example.com/"
             streaming_key: aabbccddeeff
         title: Team Event
+        training_participation_report:
+          checkpoint_interval:
+            after: 300
+            within: 400
+          initial_checkpoint_delay:
+            after: 100
+            within: 200
         type: single
         updated_at: "2024-07-20T14:16:19Z"
         updated_by:
@@ -3666,6 +3701,13 @@ components:
             timezone: Europe/Berlin
           status: ok
           title: Team Event
+          training_participation_report:
+            checkpoint_interval:
+              after: 300
+              within: 400
+            initial_checkpoint_delay:
+              after: 100
+              within: 200
           type: recurring
           updated_at: "2024-07-20T14:16:19Z"
           updated_by:
@@ -4037,6 +4079,13 @@ components:
         title:
           $ref: "#/components/schemas/EventTitle"
           description: Patch the title of th event
+        training_participation_report:
+          $ref: "#/components/schemas/TrainingParticipationReportParameterSet"
+          description: |-
+            The training participation report parameter set for the event.
+
+            When present, the training participation report will be started
+            automatically in the meeting.
         waiting_room:
           type: boolean
           description: Patch the presence of a waiting room
@@ -4243,6 +4292,13 @@ components:
         title:
           $ref: "#/components/schemas/EventTitle"
           description: Title of the event
+        training_participation_report:
+          $ref: "#/components/schemas/TrainingParticipationReportParameterSet"
+          description: |-
+            The training participation report parameter set for the event.
+
+            When present, the training participation report will be started
+            automatically in the meeting.
         waiting_room:
           type: boolean
           description: Should the created event have a waiting room?
@@ -4269,6 +4325,13 @@ components:
             streaming_endpoint: "https://ingress.streaming.example.com/"
             streaming_key: aabbccddeeff
         title: Teammeeting
+        training_participation_report:
+          checkpoint_interval:
+            after: 300
+            within: 400
+          initial_checkpoint_delay:
+            after: 100
+            within: 200
         waiting_room: false
     PostInviteRequestBody:
       type: object
@@ -4983,6 +5046,26 @@ components:
     TicketToken:
       type: string
       description: A ticket token
+    TimeRange:
+      type: object
+      description: A time range within which checkpoints can be randomly created
+      required:
+        - after
+        - within
+      properties:
+        after:
+          type: integer
+          format: int64
+          description: The earliest number of seconds after which the checkpoint can be created.
+          minimum: 0
+        within:
+          type: integer
+          format: int64
+          description: "The number of seconds within which the checkpoint can be created after the `after` value."
+          minimum: 0
+      example:
+        after: 1200
+        within: 600
     TimeZone:
       type: string
       description: A time zone
@@ -4995,6 +5078,26 @@ components:
         A UTC DateTime wrapper that implements ToRedisArgs and FromRedisValue.
 
         The values are stores as unix timestamps in redis.
+    TrainingParticipationReportParameterSet:
+      type: object
+      description: The parameters for a training participant report checkpoint procedure.
+      required:
+        - initial_checkpoint_delay
+        - checkpoint_interval
+      properties:
+        checkpoint_interval:
+          $ref: "#/components/schemas/TimeRange"
+          description: The time range definition for the subsequent checkpoints.
+        initial_checkpoint_delay:
+          $ref: "#/components/schemas/TimeRange"
+          description: The time range definition for the initial checkpoint delay.
+      example:
+        checkpoint_interval:
+          after: 300
+          within: 400
+        initial_checkpoint_delay:
+          after: 100
+          within: 200
     TurnServer:
       type: object
       description: TURN access credentials for users.
diff --git a/crates/opentalk-controller-core/src/api/v1/events/instances.rs b/crates/opentalk-controller-core/src/api/v1/events/instances.rs
index 99d355075..43413092e 100644
--- a/crates/opentalk-controller-core/src/api/v1/events/instances.rs
+++ b/crates/opentalk-controller-core/src/api/v1/events/instances.rs
@@ -42,6 +42,7 @@ use opentalk_types_api_v1::{
 use opentalk_types_common::{
     events::{invites::EventInviteStatus, EventId},
     shared_folders::SharedFolder,
+    training_participation_report::TrainingParticipationReportParameterSet,
 };
 use rrule::RRuleSet;
 
@@ -151,8 +152,16 @@ async fn get_event_instances_inner(
 
     let mut conn = db.get_conn().await?;
 
-    let (event, invite, room, sip_config, is_favorite, shared_folder, tariff) =
-        Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
+    let (
+        event,
+        invite,
+        room,
+        sip_config,
+        is_favorite,
+        shared_folder,
+        tariff,
+        training_participation_report_parameter_set,
+    ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
 
     let (invitees, invitees_truncated) =
         super::get_invitees_for_event(settings, &mut conn, event.id, invitees_max).await?;
@@ -198,6 +207,9 @@ async fn get_event_instances_inner(
         .fetch(settings, &mut conn)
         .await?;
 
+    let training_participation_report = training_participation_report_parameter_set
+        .map(TrainingParticipationReportParameterSet::from);
+
     drop(conn);
 
     let room = EventRoomInfo::from_room(settings, room, sip_config, &tariff);
@@ -225,6 +237,7 @@ async fn get_event_instances_inner(
             invitees_truncated,
             can_edit,
             shared_folder.clone(),
+            training_participation_report.clone(),
         )?;
 
         instances.push(instance);
@@ -341,8 +354,16 @@ async fn get_event_instance_inner(
 ) -> DefaultApiResult<GetEventInstanceResponseBody, CaptureApiError> {
     let mut conn = db.get_conn().await?;
 
-    let (event, invite, room, sip_config, is_favorite, shared_folder, tariff) =
-        Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
+    let (
+        event,
+        invite,
+        room,
+        sip_config,
+        is_favorite,
+        shared_folder,
+        tariff,
+        training_participation_report_parameter_set,
+    ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
     verify_recurrence_date(&event, instance_id.into())?;
 
     let (invitees, invitees_truncated) =
@@ -376,6 +397,7 @@ async fn get_event_instance_inner(
         invitees_truncated,
         can_edit,
         shared_folder,
+        training_participation_report_parameter_set.map(Into::into),
     )?;
 
     let event_instance = EventInstance {
@@ -490,8 +512,16 @@ async fn patch_event_instance_inner(
 
     let mut conn = db.get_conn().await?;
 
-    let (event, invite, room, sip_config, is_favorite, shared_folder, tariff) =
-        Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
+    let (
+        event,
+        invite,
+        room,
+        sip_config,
+        is_favorite,
+        shared_folder,
+        tariff,
+        training_participation_report_parameter_set,
+    ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
 
     if !event.is_recurring.unwrap_or_default() {
         return Err(ApiError::not_found().into());
@@ -648,6 +678,7 @@ async fn patch_event_instance_inner(
         invitees_truncated,
         can_edit,
         shared_folder,
+        training_participation_report_parameter_set.map(Into::into),
     )?;
 
     let event_instance = EventInstance {
@@ -677,6 +708,7 @@ fn create_event_instance(
     invitees_truncated: bool,
     can_edit: bool,
     shared_folder: Option<SharedFolder>,
+    training_participation_report: Option<TrainingParticipationReportParameterSet>,
 ) -> opentalk_database::Result<EventInstance> {
     let mut status = EventStatus::Ok;
 
@@ -741,6 +773,7 @@ fn create_event_instance(
         is_favorite,
         can_edit,
         shared_folder,
+        training_participation_report,
     })
 }
 
@@ -849,6 +882,7 @@ mod tests {
             is_favorite: false,
             can_edit: false,
             shared_folder: None,
+            training_participation_report: None,
         };
 
         assert_eq_json!(
diff --git a/crates/opentalk-controller-core/src/api/v1/events/invites.rs b/crates/opentalk-controller-core/src/api/v1/events/invites.rs
index fc36c86a6..b750776fd 100644
--- a/crates/opentalk-controller-core/src/api/v1/events/invites.rs
+++ b/crates/opentalk-controller-core/src/api/v1/events/invites.rs
@@ -912,8 +912,16 @@ async fn delete_invite_to_event_inner(
     let mut conn = db.get_conn().await?;
 
     // TODO(w.rabl) Further DB access optimization (replacing call to get_with_invite_and_room)?
-    let (event, _invite, room, sip_config, _is_favorite, shared_folder, _tariff) =
-        Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
+    let (
+        event,
+        _invite,
+        room,
+        sip_config,
+        _is_favorite,
+        shared_folder,
+        _tariff,
+        _training_participation_report_parameter_set,
+    ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
     let streaming_targets = get_room_streaming_targets(&mut conn, room.id).await?;
 
     let created_by = if event.created_by == current_user.id {
@@ -1069,8 +1077,16 @@ async fn delete_email_invite_to_event_inner(
 
     let mut conn = db.get_conn().await?;
 
-    let (event, _invite, room, sip_config, _is_favorite, shared_folder, _tariff) =
-        Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
+    let (
+        event,
+        _invite,
+        room,
+        sip_config,
+        _is_favorite,
+        shared_folder,
+        _tariff,
+        _training_participation_report_parameter_set,
+    ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
     let streaming_targets = get_room_streaming_targets(&mut conn, room.id).await?;
 
     let created_by = if event.created_by == current_user.id {
diff --git a/crates/opentalk-controller-core/src/api/v1/events/mod.rs b/crates/opentalk-controller-core/src/api/v1/events/mod.rs
index 400a41b09..1254fc429 100644
--- a/crates/opentalk-controller-core/src/api/v1/events/mod.rs
+++ b/crates/opentalk-controller-core/src/api/v1/events/mod.rs
@@ -35,7 +35,8 @@ use opentalk_database::{Db, DbConnection};
 use opentalk_db_storage::{
     events::{
         email_invites::EventEmailInvite, shared_folders::EventSharedFolder, Event, EventException,
-        EventExceptionKind, EventInvite, NewEvent, UpdateEvent,
+        EventExceptionKind, EventInvite, EventTrainingParticipationReportParameterSet, NewEvent,
+        UpdateEvent, UpdateEventTrainingParticipationReportParameterSet,
     },
     invites::Invite,
     rooms::{NewRoom, Room, UpdateRoom},
@@ -68,6 +69,7 @@ use opentalk_types_common::{
     shared_folders::SharedFolder,
     streaming::{RoomStreamingTarget, StreamingTarget},
     time::{DateTimeTz, RecurrencePattern, TimeZone, Timestamp},
+    training_participation_report::TrainingParticipationReportParameterSet,
 };
 use rrule::{Frequency, RRuleSet};
 use serde::Deserialize;
@@ -337,6 +339,7 @@ async fn new_event_inner(
                         streaming_targets,
                         has_shared_folder: _,
                         show_meeting_details,
+                        training_participation_report
                     } if recurrence_pattern.is_empty() => {
                         create_time_independent_event(
                             settings,
@@ -351,6 +354,7 @@ async fn new_event_inner(
                             streaming_targets,
                             show_meeting_details,
                             query,
+                            training_participation_report
                         )
                             .await?
                     }
@@ -369,6 +373,7 @@ async fn new_event_inner(
                         streaming_targets,
                         has_shared_folder: _,
                         show_meeting_details,
+                        training_participation_report
                     } => {
                         create_time_dependent_event(
                             settings,
@@ -387,6 +392,7 @@ async fn new_event_inner(
                             streaming_targets,
                             show_meeting_details,
                             query,
+                            training_participation_report
                         )
                             .await?
                     }
@@ -461,6 +467,34 @@ async fn store_event_streaming_targets(
     Ok(room_streaming_targets)
 }
 
+async fn store_training_participation_report(
+    conn: &mut DbConnection,
+    event_id: EventId,
+    TrainingParticipationReportParameterSet {
+        initial_checkpoint_delay,
+        checkpoint_interval,
+    }: TrainingParticipationReportParameterSet,
+) -> Result<Option<TrainingParticipationReportParameterSet>, CaptureApiError> {
+    let initial_checkpoint_delay_after =
+        i64::try_from(initial_checkpoint_delay.after).unwrap_or(i64::MAX);
+    let initial_checkpoint_delay_within =
+        i64::try_from(initial_checkpoint_delay.within).unwrap_or(i64::MAX);
+    let checkpoint_interval_after = i64::try_from(checkpoint_interval.after).unwrap_or(i64::MAX);
+    let checkpoint_interval_within = i64::try_from(checkpoint_interval.within).unwrap_or(i64::MAX);
+
+    let inserted = EventTrainingParticipationReportParameterSet {
+        event_id,
+        initial_checkpoint_delay_after,
+        initial_checkpoint_delay_within,
+        checkpoint_interval_after,
+        checkpoint_interval_within,
+    }
+    .try_insert(conn)
+    .await?;
+
+    Ok(inserted.map(Into::into))
+}
+
 struct MailResource {
     pub current_user: User,
     pub event: Event,
@@ -483,6 +517,7 @@ async fn create_time_independent_event(
     streaming_targets: Vec<StreamingTarget>,
     show_meeting_details: bool,
     query: EventOptionsQuery,
+    training_participation_report: Option<TrainingParticipationReportParameterSet>,
 ) -> Result<(EventResource, Option<MailResource>), CaptureApiError> {
     let room = NewRoom {
         created_by: current_user.id,
@@ -521,6 +556,12 @@ async fn create_time_independent_event(
     let streaming_targets =
         store_event_streaming_targets(conn, event.id, streaming_targets).await?;
 
+    let training_participation_report = if let Some(parameters) = training_participation_report {
+        store_training_participation_report(conn, event.id, parameters).await?
+    } else {
+        None
+    };
+
     let tariff = Tariff::get_by_user_id(conn, &current_user.id).await?;
 
     let suppress_email_notification = is_adhoc || query.suppress_email_notification;
@@ -557,6 +598,7 @@ async fn create_time_independent_event(
             shared_folder: None,
             streaming_targets,
             show_meeting_details,
+            training_participation_report,
         },
         mail_resource,
     ))
@@ -581,6 +623,7 @@ async fn create_time_dependent_event(
     streaming_targets: Vec<StreamingTarget>,
     show_meeting_details: bool,
     query: EventOptionsQuery,
+    training_participation_report: Option<TrainingParticipationReportParameterSet>,
 ) -> Result<(EventResource, Option<MailResource>), CaptureApiError> {
     let recurrence_pattern = recurrence_pattern.to_multiline_string();
 
@@ -667,6 +710,7 @@ async fn create_time_dependent_event(
             shared_folder: None,
             streaming_targets,
             show_meeting_details,
+            training_participation_report,
         },
         mail_resource,
     ))
@@ -774,7 +818,7 @@ async fn get_events_inner(
     )
     .await?;
 
-    for (event, _, _, _, exceptions, _, _, _) in &events {
+    for (event, _, _, _, exceptions, _, _, _, _) in &events {
         users.add(event);
         users.add(exceptions);
     }
@@ -812,7 +856,17 @@ async fn get_events_inner(
     let mut ret_cursor_data = None;
 
     for (
-        (event, invite, room, sip_config, exceptions, is_favorite, shared_folder, tariff),
+        (
+            event,
+            invite,
+            room,
+            sip_config,
+            exceptions,
+            is_favorite,
+            shared_folder,
+            tariff,
+            training_participation_report,
+        ),
         (mut invites_with_user, mut email_invites),
     ) in events.into_iter().zip(invites_grouped_by_event)
     {
@@ -887,6 +941,7 @@ async fn get_events_inner(
             shared_folder,
             streaming_targets: Vec::new(),
             show_meeting_details: event.show_meeting_details,
+            training_participation_report,
         }));
 
         for exception in exceptions {
@@ -997,8 +1052,16 @@ async fn get_event_inner(
 ) -> DefaultApiResult<EventResource, CaptureApiError> {
     let mut conn = db.get_conn().await?;
 
-    let (event, invite, room, sip_config, is_favorite, shared_folder, tariff) =
-        Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
+    let (
+        event,
+        invite,
+        room,
+        sip_config,
+        is_favorite,
+        shared_folder,
+        tariff,
+        training_participation_report,
+    ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
     let room_streaming_targets = get_room_streaming_targets(&mut conn, room.id).await?;
     let (invitees, invitees_truncated) =
         get_invitees_for_event(settings, &mut conn, event_id, query.invitees_max).await?;
@@ -1050,6 +1113,7 @@ async fn get_event_inner(
         shared_folder,
         streaming_targets: room_streaming_targets,
         show_meeting_details: event.show_meeting_details,
+        training_participation_report: training_participation_report.map(Into::into),
     };
 
     let event_resource = EventResource {
@@ -1157,8 +1221,16 @@ async fn patch_event_inner(
 
     let mut conn = db.get_conn().await?;
 
-    let (event, invite, room, sip_config, is_favorite, shared_folder, tariff) =
-        Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
+    let (
+        event,
+        invite,
+        room,
+        sip_config,
+        is_favorite,
+        shared_folder,
+        tariff,
+        training_participation_report,
+    ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
 
     let room = if patch.password.is_some() || patch.waiting_room.is_some() {
         // Update the event's room if at least one of the fields is set
@@ -1197,6 +1269,19 @@ async fn patch_event_inner(
         None => {}
     }
 
+    let training_participation_report = match &patch.training_participation_report {
+        Some(Some(parameter_set)) => Some(
+            UpdateEventTrainingParticipationReportParameterSet::from(parameter_set.clone())
+                .apply(&mut conn, event.id)
+                .await?,
+        ),
+        Some(None) => {
+            EventTrainingParticipationReportParameterSet::delete_by_id(&mut conn, event.id).await?;
+            None
+        }
+        None => training_participation_report,
+    };
+
     // Special case: if the patch only modifies the password do not update the event
     let event = if patch.only_modifies_room() {
         event
@@ -1296,6 +1381,7 @@ async fn patch_event_inner(
         shared_folder: shared_folder.clone(),
         streaming_targets: streaming_targets.clone(),
         show_meeting_details: event.show_meeting_details,
+        training_participation_report: training_participation_report.map(Into::into),
     };
 
     if send_email_notification {
@@ -1602,8 +1688,16 @@ async fn delete_event_inner(
     let mut conn = db.get_conn().await?;
 
     // TODO(w.rabl) Further DB access optimization (replacing call to get_with_invite_and_room)?
-    let (event, _invite, room, sip_config, _is_favorite, shared_folder, _tariff) =
-        Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
+    let (
+        event,
+        _invite,
+        room,
+        sip_config,
+        _is_favorite,
+        shared_folder,
+        _tariff,
+        _training_participation_report,
+    ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
 
     let streaming_targets = get_room_streaming_targets(&mut conn, room.id).await?;
 
@@ -1981,7 +2075,10 @@ mod tests {
     use std::time::SystemTime;
 
     use opentalk_test_util::assert_eq_json;
-    use opentalk_types_common::{events::invites::InviteRole, rooms::RoomId, users::UserId};
+    use opentalk_types_common::{
+        events::invites::InviteRole, rooms::RoomId, training_participation_report::TimeRange,
+        users::UserId,
+    };
 
     use super::*;
 
@@ -2058,6 +2155,16 @@ mod tests {
             shared_folder: None,
             streaming_targets: Vec::new(),
             show_meeting_details: true,
+            training_participation_report: Some(TrainingParticipationReportParameterSet {
+                initial_checkpoint_delay: TimeRange {
+                    after: 100,
+                    within: 200,
+                },
+                checkpoint_interval: TimeRange {
+                    after: 300,
+                    within: 400,
+                },
+            }),
         };
 
         assert_eq_json!(
@@ -2124,6 +2231,16 @@ mod tests {
                 "can_edit": true,
                 "is_adhoc": false,
                 "show_meeting_details": true,
+                "training_participation_report": {
+                    "initial_checkpoint_delay": {
+                        "after": 100,
+                        "within": 200,
+                    },
+                    "checkpoint_interval": {
+                        "after": 300,
+                        "within": 400,
+                    },
+                },
             }
         );
     }
@@ -2185,6 +2302,16 @@ mod tests {
             shared_folder: None,
             streaming_targets: Vec::new(),
             show_meeting_details: false,
+            training_participation_report: Some(TrainingParticipationReportParameterSet {
+                initial_checkpoint_delay: TimeRange {
+                    after: 100,
+                    within: 200,
+                },
+                checkpoint_interval: TimeRange {
+                    after: 300,
+                    within: 400,
+                },
+            }),
         };
 
         assert_eq_json!(
@@ -2247,6 +2374,16 @@ mod tests {
                 "can_edit": false,
                 "is_adhoc": false,
                 "show_meeting_details": false,
+                "training_participation_report": {
+                    "initial_checkpoint_delay": {
+                        "after": 100,
+                        "within": 200,
+                    },
+                    "checkpoint_interval": {
+                        "after": 300,
+                        "within": 400,
+                    },
+                },
             }
         );
     }
diff --git a/crates/opentalk-controller-service/src/controller_backend/events/shared_folder.rs b/crates/opentalk-controller-service/src/controller_backend/events/shared_folder.rs
index 0bc5c9e17..ce9cd25be 100644
--- a/crates/opentalk-controller-service/src/controller_backend/events/shared_folder.rs
+++ b/crates/opentalk-controller-service/src/controller_backend/events/shared_folder.rs
@@ -73,8 +73,16 @@ impl ControllerBackend {
 
         let (shared_folder, created) = put_shared_folder(&settings, event_id, &mut conn).await?;
 
-        let (event, _invite, room, sip_config, _is_favorite, _shared_folder, _tariff) =
-            Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
+        let (
+            event,
+            _invite,
+            room,
+            sip_config,
+            _is_favorite,
+            _shared_folder,
+            _tariff,
+            _training_participation_report,
+        ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
 
         if send_email_notification {
             let shared_folder_for_user = shared_folder_for_user(
@@ -117,8 +125,16 @@ impl ControllerBackend {
 
         let send_email_notification = !query.suppress_email_notification;
 
-        let (event, _invite, room, sip_config, _is_favorite, shared_folder, _tariff) =
-            Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
+        let (
+            event,
+            _invite,
+            room,
+            sip_config,
+            _is_favorite,
+            shared_folder,
+            _tariff,
+            _training_participation_report,
+        ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
 
         if let Some(shared_folder) = shared_folder {
             let shared_folders = std::slice::from_ref(&shared_folder);
diff --git a/crates/opentalk-controller-service/src/events/notifications.rs b/crates/opentalk-controller-service/src/events/notifications.rs
index 3d7aefa9d..fc1ba56b6 100644
--- a/crates/opentalk-controller-service/src/events/notifications.rs
+++ b/crates/opentalk-controller-service/src/events/notifications.rs
@@ -61,8 +61,16 @@ pub async fn notify_event_invitees_by_room_about_update(
     let event = Event::get_for_room(conn, room_id).await?;
 
     if let Some(event) = event {
-        let (event, _invite, room, sip_config, _is_favorite, shared_folder, _tariff) =
-            Event::get_with_related_items(conn, current_user.id, event.id).await?;
+        let (
+            event,
+            _invite,
+            room,
+            sip_config,
+            _is_favorite,
+            shared_folder,
+            _tariff,
+            _training_participation_report,
+        ) = Event::get_with_related_items(conn, current_user.id, event.id).await?;
 
         let shared_folder_for_user =
             shared_folder_for_user(shared_folder, event.created_by, current_user.id);
diff --git a/crates/opentalk-db-storage/src/events/mod.rs b/crates/opentalk-db-storage/src/events/mod.rs
index f5d7d7d87..b0d97c724 100644
--- a/crates/opentalk-db-storage/src/events/mod.rs
+++ b/crates/opentalk-db-storage/src/events/mod.rs
@@ -27,6 +27,7 @@ use opentalk_types_common::{
     sql_enum,
     tenants::TenantId,
     time::TimeZone,
+    training_participation_report::{TimeRange, TrainingParticipationReportParameterSet},
     users::UserId,
 };
 use redis_args::{FromRedisValue, ToRedisArgs};
@@ -36,8 +37,9 @@ use self::shared_folders::EventSharedFolder;
 use crate::{
     rooms::Room,
     schema::{
-        event_exceptions, event_favorites, event_invites, event_shared_folders, events, rooms,
-        sip_configs, tariffs, users,
+        event_exceptions, event_favorites, event_invites, event_shared_folders,
+        event_training_participation_report_parameter_sets, events, rooms, sip_configs, tariffs,
+        users,
     },
     sip_configs::SipConfig,
     tariffs::Tariff,
@@ -352,36 +354,43 @@ impl Event {
         bool,
         Option<EventSharedFolder>,
         Tariff,
+        Option<EventTrainingParticipationReportParameterSet>,
     )> {
-        let query = events::table
-            .left_join(
-                event_invites::table.on(event_invites::event_id
-                    .eq(events::id)
-                    .and(event_invites::invitee.eq(user_id))),
-            )
-            .left_join(
-                event_favorites::table.on(event_favorites::event_id
-                    .eq(events::id)
-                    .and(event_favorites::user_id.eq(user_id))),
-            )
-            .left_join(
-                event_shared_folders::table.on(event_shared_folders::event_id.eq(events::id)),
-            )
-            .inner_join(rooms::table.on(events::room.eq(rooms::id)))
-            .left_join(sip_configs::table.on(rooms::id.eq(sip_configs::room)))
-            .inner_join(users::table.on(users::id.eq(rooms::created_by)))
-            .inner_join(tariffs::table.on(tariffs::id.eq(users::tariff_id)))
-            .select((
-                events::all_columns,
-                event_invites::all_columns.nullable(),
-                rooms::all_columns,
-                sip_configs::all_columns.nullable(),
-                event_favorites::user_id.nullable().is_not_null(),
-                event_shared_folders::all_columns.nullable(),
-                tariffs::all_columns,
-            ))
-            .filter(events::id.eq(event_id));
-
+        let query =
+            events::table
+                .left_join(
+                    event_invites::table.on(event_invites::event_id
+                        .eq(events::id)
+                        .and(event_invites::invitee.eq(user_id))),
+                )
+                .left_join(
+                    event_favorites::table.on(event_favorites::event_id
+                        .eq(events::id)
+                        .and(event_favorites::user_id.eq(user_id))),
+                )
+                .left_join(
+                    event_shared_folders::table.on(event_shared_folders::event_id.eq(events::id)),
+                )
+                .inner_join(rooms::table.on(events::room.eq(rooms::id)))
+                .left_join(sip_configs::table.on(rooms::id.eq(sip_configs::room)))
+                .inner_join(users::table.on(users::id.eq(rooms::created_by)))
+                .inner_join(tariffs::table.on(tariffs::id.eq(users::tariff_id)))
+                .inner_join(
+                    event_training_participation_report_parameter_sets::table
+                        .on(event_training_participation_report_parameter_sets::event_id
+                            .eq(events::id)),
+                )
+                .select((
+                    events::all_columns,
+                    event_invites::all_columns.nullable(),
+                    rooms::all_columns,
+                    sip_configs::all_columns.nullable(),
+                    event_favorites::user_id.nullable().is_not_null(),
+                    event_shared_folders::all_columns.nullable(),
+                    tariffs::all_columns,
+                    event_training_participation_report_parameter_sets::all_columns.nullable(),
+                ))
+                .filter(events::id.eq(event_id));
         Ok(query.first(conn).await?)
     }
 
@@ -431,6 +440,7 @@ impl Event {
             bool,
             Option<EventSharedFolder>,
             Tariff,
+            Option<TrainingParticipationReportParameterSet>,
         )>,
     > {
         // Filter applied to all events which validates that the event is either created by
@@ -585,6 +595,8 @@ impl Event {
             } else {
                 vec![]
             };
+            // TODO: remove this
+            let training_participation_report_parameter_set = None;
 
             events_with_invite_room_and_exceptions.push((
                 event,
@@ -595,6 +607,7 @@ impl Event {
                 is_favorite,
                 shared_folders,
                 tariff,
+                training_participation_report_parameter_set,
             ));
         }
 
@@ -1132,3 +1145,139 @@ impl NewEventFavorite {
         }
     }
 }
+
+#[derive(Debug, Insertable, Queryable, Identifiable, Associations)]
+#[diesel(table_name = event_training_participation_report_parameter_sets)]
+#[diesel(primary_key(event_id))]
+#[diesel(belongs_to(Event, foreign_key = event_id))]
+pub struct EventTrainingParticipationReportParameterSet {
+    pub event_id: EventId,
+    pub initial_checkpoint_delay_after: i64,
+    pub initial_checkpoint_delay_within: i64,
+    pub checkpoint_interval_after: i64,
+    pub checkpoint_interval_within: i64,
+}
+
+impl EventTrainingParticipationReportParameterSet {
+    #[tracing::instrument(err, skip_all)]
+    pub async fn get_for_event(conn: &mut DbConnection, event_id: EventId) -> Result<Option<Self>> {
+        let parameter_set = event_training_participation_report_parameter_sets::table
+            .filter(event_training_participation_report_parameter_sets::event_id.eq(event_id))
+            .get_result(conn)
+            .await
+            .optional()?;
+
+        Ok(parameter_set)
+    }
+
+    /// Tries to insert the EventTrainingParticipationParameterSet into the database
+    ///
+    /// When yielding a unique key violation, None is returned.
+    #[tracing::instrument(err, skip_all)]
+    pub async fn try_insert(self, conn: &mut DbConnection) -> Result<Option<Self>> {
+        let query = self.insert_into(event_training_participation_report_parameter_sets::table);
+
+        let result = query.get_result(conn).await;
+
+        match result {
+            Ok(event_invite) => Ok(Some(event_invite)),
+            Err(diesel::result::Error::DatabaseError(
+                diesel::result::DatabaseErrorKind::UniqueViolation,
+                ..,
+            )) => Ok(None),
+            Err(e) => Err(e.into()),
+        }
+    }
+
+    #[tracing::instrument(err, skip_all)]
+    pub async fn delete_by_id(conn: &mut DbConnection, event_id: EventId) -> Result<()> {
+        let query = diesel::delete(
+            event_training_participation_report_parameter_sets::table
+                .filter(event_training_participation_report_parameter_sets::event_id.eq(event_id)),
+        );
+
+        query.execute(conn).await?;
+
+        Ok(())
+    }
+}
+
+impl From<EventTrainingParticipationReportParameterSet>
+    for TrainingParticipationReportParameterSet
+{
+    fn from(
+        EventTrainingParticipationReportParameterSet {
+            event_id: _,
+            initial_checkpoint_delay_after,
+            initial_checkpoint_delay_within,
+            checkpoint_interval_after,
+            checkpoint_interval_within,
+        }: EventTrainingParticipationReportParameterSet,
+    ) -> Self {
+        Self {
+            initial_checkpoint_delay: TimeRange {
+                after: u64::try_from(initial_checkpoint_delay_after).unwrap_or_default(),
+                within: u64::try_from(initial_checkpoint_delay_within).unwrap_or_default(),
+            },
+            checkpoint_interval: TimeRange {
+                after: u64::try_from(checkpoint_interval_after).unwrap_or_default(),
+                within: u64::try_from(checkpoint_interval_within).unwrap_or_default(),
+            },
+        }
+    }
+}
+
+#[derive(AsChangeset)]
+#[diesel(table_name = event_training_participation_report_parameter_sets)]
+pub struct UpdateEventTrainingParticipationReportParameterSet {
+    pub initial_checkpoint_delay_after: Option<i64>,
+    pub initial_checkpoint_delay_within: Option<i64>,
+    pub checkpoint_interval_after: Option<i64>,
+    pub checkpoint_interval_within: Option<i64>,
+}
+
+impl UpdateEventTrainingParticipationReportParameterSet {
+    /// Apply the update to the invite where `user_id` is the invitee
+    #[tracing::instrument(err, skip_all)]
+    pub async fn apply(
+        self,
+        conn: &mut DbConnection,
+        event_id: EventId,
+    ) -> Result<EventTrainingParticipationReportParameterSet> {
+        let query = diesel::update(event_training_participation_report_parameter_sets::table)
+            .filter(event_training_participation_report_parameter_sets::event_id.eq(event_id))
+            .set(self)
+            // change it here
+            .returning(event_training_participation_report_parameter_sets::all_columns);
+
+        let event_training_participation_report_parameter_sets = query.get_result(conn).await?;
+
+        Ok(event_training_participation_report_parameter_sets)
+    }
+}
+
+impl From<TrainingParticipationReportParameterSet>
+    for UpdateEventTrainingParticipationReportParameterSet
+{
+    fn from(
+        TrainingParticipationReportParameterSet {
+            initial_checkpoint_delay,
+            checkpoint_interval,
+        }: TrainingParticipationReportParameterSet,
+    ) -> Self {
+        Self {
+            initial_checkpoint_delay_after: Some(
+                i64::try_from(initial_checkpoint_delay.after).unwrap_or(i64::MAX),
+            ),
+            initial_checkpoint_delay_within: Some(
+                i64::try_from(initial_checkpoint_delay.within).unwrap_or(i64::MAX),
+            ),
+            checkpoint_interval_after: Some(
+                i64::try_from(checkpoint_interval.after).unwrap_or(i64::MAX),
+            ),
+            checkpoint_interval_within: Some(
+                i64::try_from(checkpoint_interval.within).unwrap_or(i64::MAX),
+            ),
+        }
+    }
+}
diff --git a/crates/opentalk-db-storage/src/migrations/V47__create_event_training_participation_report_parameter_sets.sql b/crates/opentalk-db-storage/src/migrations/V47__create_event_training_participation_report_parameter_sets.sql
new file mode 100644
index 000000000..c9b3ed6f9
--- /dev/null
+++ b/crates/opentalk-db-storage/src/migrations/V47__create_event_training_participation_report_parameter_sets.sql
@@ -0,0 +1,7 @@
+CREATE TABLE event_training_participation_report_parameter_sets (
+    event_id UUID PRIMARY KEY REFERENCES events(id) ON DELETE CASCADE NOT NULL,
+    initial_checkpoint_delay_after BIGINT NOT NULL,
+    initial_checkpoint_delay_within BIGINT NOT NULL,
+    checkpoint_interval_after BIGINT NOT NULL,
+    checkpoint_interval_within BIGINT NOT NULL
+);
diff --git a/crates/opentalk-db-storage/src/schema.rs b/crates/opentalk-db-storage/src/schema.rs
index f41661531..3e091cc2a 100644
--- a/crates/opentalk-db-storage/src/schema.rs
+++ b/crates/opentalk-db-storage/src/schema.rs
@@ -113,6 +113,18 @@ diesel::table! {
     }
 }
 
+diesel::table! {
+    use crate::sql_types::*;
+
+    event_training_participation_report_parameter_sets (event_id) {
+        event_id -> Uuid,
+        initial_checkpoint_delay_after -> Int8,
+        initial_checkpoint_delay_within -> Int8,
+        checkpoint_interval_after -> Int8,
+        checkpoint_interval_within -> Int8,
+    }
+}
+
 diesel::table! {
     use crate::sql_types::*;
 
@@ -384,6 +396,7 @@ diesel::joinable!(event_favorites -> events (event_id));
 diesel::joinable!(event_favorites -> users (user_id));
 diesel::joinable!(event_invites -> events (event_id));
 diesel::joinable!(event_shared_folders -> events (event_id));
+diesel::joinable!(event_training_participation_report_parameter_sets -> events (event_id));
 diesel::joinable!(events -> rooms (room));
 diesel::joinable!(events -> tenants (tenant_id));
 diesel::joinable!(external_tariffs -> tariffs (tariff_id));
@@ -413,6 +426,7 @@ diesel::allow_tables_to_appear_in_same_query!(
     event_favorites,
     event_invites,
     event_shared_folders,
+    event_training_participation_report_parameter_sets,
     events,
     external_tariffs,
     groups,
diff --git a/crates/opentalk-signaling-module-training-participation-report/src/lib.rs b/crates/opentalk-signaling-module-training-participation-report/src/lib.rs
index 2361ae8d3..d9f58596c 100644
--- a/crates/opentalk-signaling-module-training-participation-report/src/lib.rs
+++ b/crates/opentalk-signaling-module-training-participation-report/src/lib.rs
@@ -52,6 +52,7 @@ use opentalk_types_common::{
     modules::ModuleId,
     rooms::RoomId,
     time::{TimeZone, Timestamp},
+    training_participation_report::TimeRange,
     users::{DisplayName, UserId},
 };
 use opentalk_types_signaling::ParticipantId;
@@ -63,7 +64,7 @@ use opentalk_types_signaling_training_participation_report::{
         PresenceLoggingStartedReason, TrainingParticipationReportEvent,
     },
     state::{ParticipationLoggingState, TrainingParticipationReportState},
-    TimeRange, MODULE_ID,
+    MODULE_ID,
 };
 use rand::Rng as _;
 use snafu::{Report, ResultExt as _};
@@ -209,6 +210,8 @@ impl TrainingParticipationReport {
             .get_recorded_presence_state(self.room, self.participant)
             .await?;
 
+        let parameter_set = None;
+
         if control_data.is_room_owner {
             let (other_room_owners, trainees) = self
                 .load_already_present_room_owners_and_trainees(
@@ -225,8 +228,17 @@ impl TrainingParticipationReport {
                 // Don't ask the room owner for confirmation
                 state = ParticipationLoggingState::Enabled;
             }
+
+            *frontend_data = Some(TrainingParticipationReportState {
+                state,
+                parameter_set,
+            });
+        } else {
+            *frontend_data = Some(TrainingParticipationReportState {
+                state,
+                parameter_set: None,
+            });
         }
-        *frontend_data = Some(TrainingParticipationReportState { state });
         Ok(())
     }
 
diff --git a/crates/opentalk-signaling-module-training-participation-report/src/storage/mod.rs b/crates/opentalk-signaling-module-training-participation-report/src/storage/mod.rs
index 3f0a19e1b..655a0e75c 100644
--- a/crates/opentalk-signaling-module-training-participation-report/src/storage/mod.rs
+++ b/crates/opentalk-signaling-module-training-participation-report/src/storage/mod.rs
@@ -19,11 +19,9 @@ mod test_common {
     use std::collections::{BTreeMap, BTreeSet};
 
     use opentalk_signaling_core::SignalingModuleError;
-    use opentalk_types_common::rooms::RoomId;
+    use opentalk_types_common::{rooms::RoomId, training_participation_report::TimeRange};
     use opentalk_types_signaling::ParticipantId;
-    use opentalk_types_signaling_training_participation_report::{
-        state::ParticipationLoggingState, TimeRange,
-    };
+    use opentalk_types_signaling_training_participation_report::state::ParticipationLoggingState;
     use pretty_assertions::assert_eq;
 
     use super::TrainingParticipationReportStorage;
diff --git a/crates/opentalk-signaling-module-training-participation-report/src/storage/redis.rs b/crates/opentalk-signaling-module-training-participation-report/src/storage/redis.rs
index 2c7a0e153..9bf44eddd 100644
--- a/crates/opentalk-signaling-module-training-participation-report/src/storage/redis.rs
+++ b/crates/opentalk-signaling-module-training-participation-report/src/storage/redis.rs
@@ -6,11 +6,11 @@ use std::collections::{BTreeMap, BTreeSet};
 
 use async_trait::async_trait;
 use opentalk_signaling_core::{NotFoundSnafu, RedisConnection, RedisSnafu, SignalingModuleError};
-use opentalk_types_common::{rooms::RoomId, time::Timestamp};
-use opentalk_types_signaling::ParticipantId;
-use opentalk_types_signaling_training_participation_report::{
-    state::ParticipationLoggingState, TimeRange,
+use opentalk_types_common::{
+    rooms::RoomId, time::Timestamp, training_participation_report::TimeRange,
 };
+use opentalk_types_signaling::ParticipantId;
+use opentalk_types_signaling_training_participation_report::state::ParticipationLoggingState;
 use redis::{AsyncCommands, ExistenceCheck, SetOptions};
 use redis_args::{FromRedisValue, ToRedisArgs};
 use serde::{Deserialize, Serialize};
diff --git a/crates/opentalk-signaling-module-training-participation-report/src/storage/room_state.rs b/crates/opentalk-signaling-module-training-participation-report/src/storage/room_state.rs
index 16e734e91..ce960b89a 100644
--- a/crates/opentalk-signaling-module-training-participation-report/src/storage/room_state.rs
+++ b/crates/opentalk-signaling-module-training-participation-report/src/storage/room_state.rs
@@ -4,9 +4,8 @@
 
 use std::collections::BTreeSet;
 
-use opentalk_types_common::time::Timestamp;
+use opentalk_types_common::{time::Timestamp, training_participation_report::TimeRange};
 use opentalk_types_signaling::ParticipantId;
-use opentalk_types_signaling_training_participation_report::TimeRange;
 
 use super::{Checkpoint, TrainingReportState};
 
diff --git a/crates/opentalk-signaling-module-training-participation-report/src/storage/training_participation_report_storage.rs b/crates/opentalk-signaling-module-training-participation-report/src/storage/training_participation_report_storage.rs
index 83648365e..1d0f5d87c 100644
--- a/crates/opentalk-signaling-module-training-participation-report/src/storage/training_participation_report_storage.rs
+++ b/crates/opentalk-signaling-module-training-participation-report/src/storage/training_participation_report_storage.rs
@@ -6,11 +6,11 @@ use std::collections::BTreeSet;
 
 use async_trait::async_trait;
 use opentalk_signaling_core::SignalingModuleError;
-use opentalk_types_common::{rooms::RoomId, time::Timestamp};
-use opentalk_types_signaling::ParticipantId;
-use opentalk_types_signaling_training_participation_report::{
-    state::ParticipationLoggingState, TimeRange,
+use opentalk_types_common::{
+    rooms::RoomId, time::Timestamp, training_participation_report::TimeRange,
 };
+use opentalk_types_signaling::ParticipantId;
+use opentalk_types_signaling_training_participation_report::state::ParticipationLoggingState;
 
 use super::{RoomState, TrainingReportState};
 
diff --git a/crates/opentalk-signaling-module-training-participation-report/src/storage/volatile/memory.rs b/crates/opentalk-signaling-module-training-participation-report/src/storage/volatile/memory.rs
index 998f2ff60..2b2b4d85c 100644
--- a/crates/opentalk-signaling-module-training-participation-report/src/storage/volatile/memory.rs
+++ b/crates/opentalk-signaling-module-training-participation-report/src/storage/volatile/memory.rs
@@ -5,11 +5,11 @@
 use std::collections::{BTreeMap, BTreeSet};
 
 use opentalk_signaling_core::{NotFoundSnafu, SignalingModuleError};
-use opentalk_types_common::{rooms::RoomId, time::Timestamp};
-use opentalk_types_signaling::ParticipantId;
-use opentalk_types_signaling_training_participation_report::{
-    state::ParticipationLoggingState, TimeRange,
+use opentalk_types_common::{
+    rooms::RoomId, time::Timestamp, training_participation_report::TimeRange,
 };
+use opentalk_types_signaling::ParticipantId;
+use opentalk_types_signaling_training_participation_report::state::ParticipationLoggingState;
 use snafu::{ensure_whatever, OptionExt as _};
 
 use crate::storage::{Checkpoint, RoomState, TrainingReportState};
diff --git a/crates/opentalk-signaling-module-training-participation-report/src/storage/volatile/storage.rs b/crates/opentalk-signaling-module-training-participation-report/src/storage/volatile/storage.rs
index 40355229a..95ebf1e25 100644
--- a/crates/opentalk-signaling-module-training-participation-report/src/storage/volatile/storage.rs
+++ b/crates/opentalk-signaling-module-training-participation-report/src/storage/volatile/storage.rs
@@ -9,11 +9,11 @@ use std::{
 
 use async_trait::async_trait;
 use opentalk_signaling_core::{SignalingModuleError, VolatileStaticMemoryStorage};
-use opentalk_types_common::{rooms::RoomId, time::Timestamp};
-use opentalk_types_signaling::ParticipantId;
-use opentalk_types_signaling_training_participation_report::{
-    state::ParticipationLoggingState, TimeRange,
+use opentalk_types_common::{
+    rooms::RoomId, time::Timestamp, training_participation_report::TimeRange,
 };
+use opentalk_types_signaling::ParticipantId;
+use opentalk_types_signaling_training_participation_report::state::ParticipationLoggingState;
 use parking_lot::RwLock;
 
 use super::memory::TrainingParticipationReportState;
diff --git a/deny.toml b/deny.toml
index adf627c2f..428373243 100644
--- a/deny.toml
+++ b/deny.toml
@@ -322,7 +322,6 @@ skip = [
   { name = "indexmap", version = "1" },
   { name = "itertools", version = "0.10" },
   { name = "itertools", version = "0.11" },
-  { name = "itertools", version = "0.12" },
   { name = "jiff", version = "0.1" },
   { name = "linux-raw-sys", version = "0.4" },
   { name = "p256", version = "0.11" },
diff --git a/docs/developer/database.md b/docs/developer/database.md
index 8783ef729..b885582a5 100644
--- a/docs/developer/database.md
+++ b/docs/developer/database.md
@@ -75,6 +75,13 @@ event_shared_folders {
     text write_share_id
     text write_url
 }
+event_training_participation_report_parameter_sets {
+    uuid event_id PK,FK
+    bigint checkpoint_interval_after
+    bigint checkpoint_interval_within
+    bigint initial_checkpoint_delay_after
+    bigint initial_checkpoint_delay_within
+}
 events {
     uuid id PK
     uuid created_by FK
@@ -241,6 +248,7 @@ event_invites }o--|| events: ""
 event_invites }o--|| users: ""
 event_invites }o--|| users: ""
 event_shared_folders |o--|| events: ""
+event_training_participation_report_parameter_sets |o--|| events: ""
 events }o--|| rooms: ""
 events }o--|| users: ""
 events }o--|| users: ""
-- 
GitLab