pkONE® : VzDocumentation

1. Overview

This document is the starting point for VZ-specific handbook content.

Customer-specific chapters and generated business specifications can be added following the same structure used for the other customer documentations.

2. DocumentStore - Verbesserte Benutzerführung und UI

2.1. Ausgangslage & Problemanalyse

Der DocumentStore soll verbessert werden. Ziel ist es, dass Kunden intuitiv erkennen, welche Dokumente neu zur Verfügung gestellt wurden und welche Dokumente schon bezogen wurden. Zudem soll das Layout übersichtlicher gestaltet werden.

Ein kritisches Beispiel ist der Versand von Dokumentbündeln, zum Beispiel Faktura und Merkblätter: Benutzer finden die wichtigen Dokumente, wie die Faktura, nach dem Klick auf eine Benachrichtigung teilweise nicht.

Gründe für die mangelnde Benutzerführung:

  • Information Overload: Durch die standardmässige Anzeige aller Sprachen ist die Ansicht überladen.

  • Fehlende Deep-Links in Notifizierung: Benachrichtigungen führen nicht direkt zum spezifischen Dokument oder zur Kategorie, sondern nur in die allgemeine Kategorie, wodurch der Bezug verloren geht.

  • UI & Layout: Kategorien werden in einem Drop-down angezeigt, was die Orientierung erschwert.

  • Unklarer Status: Da ein klarer "Gelesen"- oder "Heruntergeladen"-Status fehlt, erkennt der Benutzer nicht, welche Dokumente neu oder relevant sind.

2.2. Funktionale Anpassungen

2.2.1. Automatische Sprachfilterung (#7873)

Um die Relevanz der angezeigten Dokumente beim Einstieg zu erhöhen und den Overload zu reduzieren:

  • Default-Sprachfilter: Beim Öffnen des DocumentStore wird der Filter automatisch auf die Sprache des Mandanten gesetzt.

  • Ausnahme Multi-Mandanten: Hat ein User Zugriff auf mehrere Mandanten mit unterschiedlichen Sprachen, bleibt der Initialfilter leer ("Show all"), um Fehlinterpretationen zu vermeiden.

  • Manueller Override: Der User kann die Sprache jederzeit via Dropdown ändern, um zum Beispiel allgemeine Reglemente in seiner persönlichen Portalsprache zu suchen.

E-Mail-Benachrichtigungen sollen einen Deep Link enthalten, wodurch direkt auf das Dokument oder auch nur auf eine Kategorie, zum Beispiel Rechnungen, verlinkt werden kann.

2.2.3. Kategorien-Management & UI Redesign

Wechsel vom alten VZ-Design auf das neue Design:

  • Seitenpanel: Kategorien werden links in einem Panel angezeigt statt in einem Dropdown. Somit werden auf einen Blick alle Kategorien präsentiert. Dadurch wird die Übersicht für den Benutzer deutlich erhöht. Ausserdem ist diese Anpassung Grundlage dafür, dass Badges angebracht werden können.

  • Tabellen-Layout: Optimierung der Breite, bessere Anordnung der Spalten, grössere Schrift, nicht benötigte Spalten entfernen, zum Beispiel Grösse.

2.2.4. Status-Tracking & Badges

Einführung von Badges und neuen Dokument-Status:

  • Badge auf Kategorie: Auf der Kategorie, zum Beispiel "Rechnungen", erscheint ein Badge mit der Anzahl der ungelesenen Dokumente, sodass für den Benutzer sofort ersichtlich wird, in welcher Kategorie neue Dokumente verteilt wurden.

  • Schriftart auf ungelesenem Dokument: Ein ungelesenes Dokument wird farblich hervorgehoben, zum Beispiel mit fetter Schrift, dunklerer Schrift oder orangem Icon, um den Benutzer noch klarer zu führen.

  • Neue Dokument-Status, Anpassung der Icon-Logik:

    • Ungelesenes Dokument: Geschlossener Briefumschlag

    • Gelesenes Dokument: Geöffneter Briefumschlag

      • Trigger: Der Status wechselt von "ungelesen" auf "gelesen", sobald das Dokument heruntergeladen oder als gelesen markiert wurde.

    • Heruntergeladenes Dokument: Download-Icon, noch festzulegen

      • Trigger: Der Status wechselt von "gelesen/ungelesen" auf "heruntergeladen", sobald das Dokument mittels Aktion "herunterladen" geladen wurde.

DocumentStore mockup

Das folgende Mockup zeigt die geplante Neugestaltung des DocumentStores. Bitte beachten, dass es sich um einen Entwurf handelt, dessen finale Umsetzung von dieser Darstellung abweichen kann.

2.2.5. Erweiterung Aufgaben Panel "MyTasks"

Um sicherzustellen, dass wichtige Dokumente, wie die Faktura, nicht untergehen, wird das Modul "MyTasks" eingeführt und um Dokumenttypen erweitert:

  • Konfiguration pro Dokumenttyp: Im Backend kann pro Dokumenttyp, zum Beispiel Typ "Faktura", definiert werden: Erzeugt Task = Ja/Nein.

  • Workflow: Wird ein solches Dokument via Distribution verteilt, erscheint auf dem Open Tasks Panel auf der Startseite des Arbeitgebers automatisch ein neuer Task, zum Beispiel "Neue Rechnung zur Prüfung".

  • Deep-Link: Der Task führt direkt zum gefilterten DocumentStore.

  • Abschluss: Der Task verschwindet erst, wenn das Dokument im DocumentStore geöffnet, heruntergeladen oder als erledigt gesetzt wurde.

2.3. Offene Punkte (Abklärung Fabio/Luana)

  • Automatisierung Upload: Prüfung, ob der manuelle Upload (Formulare, Merkblätter, Studien, Reglemente) durch eine automatisierte Distribution ersetzt werden kann, um Aufwand zu reduzieren, Prozesse zu vereinheitlichen und Upload-Fehler zu minimieren (#7873).

3. MyTasks

3.1. Vision & Zielsetzung

Das Panel "Meine Aufgaben" optimiert die Benutzerführung im Portal für Arbeitgeber, Versicherte und Sachbearbeiter. Durch die zentrale Anzeige aller offenen Aufgaben wird der Nutzer optimal durch seine anstehenden Arbeiten geführt, von einfachen Mutationen über den Erhalt neuer Dokumente bis hin zur Lohnerfassung.

Besonders bei komplexen Geschäftsfällen wie zum Beispiel Meldung von Arbeitsunfähigkeit, Invalidität oder Todesfällen bietet das neue Panel Unterstützung durch optimale Führung des Benutzers. Umfangreiche Prozesse können in übersichtliche Einzelschritte und Subtasks aufgebrochen werden. Diese Führung stellt sicher, dass auch komplexe Prozesse korrekt und effizient bearbeitet werden. Durch diese verbesserte Benutzerführung können zukünftig auch komplexe Mutationen vollständig ins Portal übertragen werden.

Darüber hinaus bietet das Panel folgende Vorteile:

  • Volle Transparenz: Benutzer sehen auf einen Blick, welche Aufgaben im Portal noch erledigt werden müssen.

  • Aktive Prozesssteuerung: Durch die direkte Zuweisung werden Pendenzen schneller abgearbeitet und Prozesse aktiv vorangetrieben.

  • Gezielte Nutzerführung und Effizienz: Der Benutzer wird strukturiert durch seine To-dos geführt und kann seine Aufgaben so effizient im Portal abarbeiten.

Das neue Panel wird eine wertvolle Komponente in pkONE, hier ein paar Beispiele:

Rolle Task Use Case

AG

Mutation

Offene Mutationen werden angezeigt und so nicht vergessen

AG

Lohnerfassung

Sobald die jährliche Lohnmeldung initialisiert wird, wird ein Task ausgelöst

AG

Lohnerfassung

Folgemutationen innerhalb der Lohnerfassung lösen einen Task aus und gehen so nicht vergessen

AG

Mutation (mit Flag)

Komplexe Mutationen, die einen erweiterten Prozess erfordern, zum Beispiel Arbeitsunfähigkeit, lösen eine Aufgabe aus

AG

Document Task

Verteilung eines bestimmten Dokumenttyps löst einen Task aus, sodass sichergestellt wird, dass das Dokument verarbeitet wurde, sinnvoll bei wichtigen Dokumenten wie Faktura oder bei grösseren Arbeitgeberanschlüssen

VS

Mutation

Bei Neueintritt wird ein Task zur Wahl des Sparplans generiert

VS

Validierung

Versicherte werden über einen Task informiert, wenn Kontaktdaten verifiziert werden müssen

usw…​

3.2. Mockup

MyTasks panel mockup 1

3.3. Task-Hierarchie & Struktur

Um komplexe Geschäftsprozesse übersichtlich abzubilden, nutzt das System eine zweistufige Task-Architektur.

  • Haupttask (Parent): Repräsentiert den übergeordneten Prozess, zum Beispiel "Lohnerfassung" oder eine reguläre "Mutation Austritt".

  • Untertask (Children): Steht für granulare Aktionen innerhalb eines Prozesses, zum Beispiel "Verhandelbare Folgemutation".

  • Verknüpfung: Jeder Untertask ist technisch zwingend mit einem Haupttask als "Parent" verlinkt.

3.4. UI & Anzeige-Verhalten

3.4.1. Basis-Design und Interaktion

  • Standard-Ansicht: Das Panel zeigt initial ausschliesslich Aufgaben mit dem Status offen.

  • Navigation: Ein Klick auf "Aufgabe öffnen" führt via Deep Link (Action-Framework) direkt zur Ziel-Entität, zum Beispiel Mutations-Detailseite oder Dokument, InAppMail etc.

  • Scroll-Verhalten: Das Panel erhält eine Maximalhöhe mit vertikalem Scrollbar für die Aufgabenliste.

3.4.2. Kontextuelle Einbindung

  • Startseite (Dashboard): Zeigt global alle offenen Haupttasks über sämtliche Typen hinweg an. Untertasks werden hier ausgeblendet, um die Übersichtlichkeit zu wahren.

  • Themenbezogene Einbettung: Das Panel kann themenbezogen auf anderen Seiten eingebettet werden. Am Beispiel der Maske "Lohnerfassung": Hier ist das Panel erst sichtbar, sobald ein Mandant ausgewählt wurde. Es ist gefiltert auf den Typ "Lohnerfassung" und zeigt den Haupttask sowie die zugehörigen Untertasks, also offene verhandelbare Folgemutationen, an.

  • Das Panel kann an diversen weiteren Stellen im Portal eingeblendet werden.

3.5. Logik, Lebenszyklus & Automatisierung

3.5.1. Trigger für die Aufgabenerstellung

  • Einfache Mutationen: Mutationen, die gespeichert werden, aber nicht eingereicht, generieren automatisch einen Task.

  • Prozess-Initialisierung: Beispielsweise löst die Initialisierung der Lohnerfassung den LOM-Haupttask aus.

  • Komplexe Mutationen (AUF/Todesfall): Speichert oder meldet der Arbeitgeber eine Mutation, die weitere Aktionen erfordert, also geflaggte Mutationen, generiert das System sofort einen Task. Benötigte Formulare werden vom Sachbearbeiter oder System direkt im DocumentStore mit der Mutation verknüpft und einem Task zugewiesen.

  • Dokument-Task: Sobald spezifische Dokumente, zum Beispiel Rechnungen oder Informationsschreiben mit Lesebestätigung, verteilt werden, entsteht eine Aufgabe im Portal, konfigurierbar über Dokumenttyp.

3.5.2. Fälligkeiten (Due Dates)

  • Tasks können ein Due Date (Fälligkeitsdatum) anzeigen.

  • Bei der Lohnerfassung entspricht dies dem Enddatum der für den Mandanten freigeschalteten Periode.

    • Ist für einen Mandanten keine Periode hinterlegt, entfällt das Due Date oder greift auf einen noch zu definierenden Standardwert zurück.

3.5.3. Erledigung und Löschung (Abschluss)

Anzeige: Tasks verschwinden standardmässig aus der "Offen"-Ansicht, sobald der Status auf erledigt wechselt.

  • Explizit: Manuelles Markieren als "erledigt" oder "done" durch den User.

  • Implizit: Das System schliesst Tasks automatisch, wenn eine Bedingung erfüllt ist, zum Beispiel wenn ein zwingendes Dokument heruntergeladen oder eine Mutation eingereicht wurde.

3.5.4. Benachrichtigung

Jeder Task hat eine Benachrichtigungsdefinition hinterlegt. So kann für den jeweiligen Task konfiguriert werden, wie viele Notifizierungen in welchen Abständen verschickt werden sollen.

3.6. Spezialfall: Jährliche Lohnerfassung (LE)

Die LE unterliegt einer abweichenden, zeitgesteuerten Anzeigelogik.

  • Sichtbarkeit: Der LE-Haupttask auf dem Dashboard wird während der aktiven Periode angezeigt.

  • Abschluss-Verhalten: Werden alle LE-Datensätze und offenen Folgemutationen bestätigt, gilt die LE als erledigt.

  • Persistenz: Im Gegensatz zu regulären Tasks verschwindet der LE-Haupttask nach Erledigung nicht sofort. Er bleibt bis zum Ende der Periode sichtbar und wird farblich als "abgeschlossen/eingereicht" markiert.

MyTasks panel mockup 2

3.7. Technische Beschreibung

3.7.1. Task

  • Offene Aufgabe, Attribute

    • Status: offen/geschlossen

    • Bezeichnung

    • Beschreibung

    • Fällig am (due date)

    • Erstellt am (created date)

    • Erledigt an (done date)

    • Erledigt durch

3.7.2. Action

Einführung von Action als 1st-class-Entity: Eine Verlinkung innerhalb pkONE, die über Parameter eine spezifische Entity adressieren kann. Eine Action kann von überall her getriggert werden. Siehe auch ActionToClassNavigationMapping für das aktuelle Mapping.

Beispiele:

  • IAM: Aufruf einer IAM mit vorgewähltem Topic, Subject und Content

  • Mutation: Öffnen einer Mutation mit entsprechender Sektion geöffnet

3.7.3. Maintenance

  • Alle erledigten Tasks werden nach x Tagen gelöscht.

3.8. Roadmap & Versionierung

Version Geplante Funktionen

V1 (Aktuell)

Einführung Panel im Arbeitgeberportal (HomeScreen) und Mandantentasks, Einbindung in Lohnerfassungsmaske, Parent-Child-Hierarchie, LOM-Speziallogik, Action-Framework

V2 (Zukunft - basierend auf Kundenanforderungen)

Anzeige Panel an weiteren Stellen im Arbeitgeberportal, aktives Benachrichtigungssystem (E-Mail oder Push-Notifikationen bei Generierung neuer Tasks), explizite Erledigung Task, Erweiterung der Dokumenten-Trigger via DocumentTypes-Konfiguration, Panel für Sachbearbeiter, Panel für Versichertenportal

4. Audit History

Kunden wünschen sich bessere Nachvollziehbarkeit von Änderungen (to be audit compliant)

4.1. Fragen

  • Wie lange müssen die History Daten zur Verfügung stehen? (Verfallsdatum)

  • Wieviel Datenvolumen fällt an? Evtl. müssen wir DiskSpace erhöhen

  • Darstellung der Detail-View als normale View oder Modalwindow

4.2. Story

Zur besseren Nachvollziehbarkeit soll für die Rolle „Admin“ eine Änderungshistorie im Bereich „Benutzer“ implementiert werden. Admins sollen einsehen können, welche Werte von welchem Benutzer zu welchem Datum und zu welcher Uhrzeit geändert wurden.

4.3. Main View

Finding the new Audit view:

Mock menu

Mock how the list view could look like:

Mock list view

Neue Maske "Audit History" (Zugriff nur für Rolle Admin)

Search _______________________ (freetext: related entity, Creator)

Typ _____________________^

Date von ___________ bis _____________

Typ Related entity Creator Date Modifier Detail

USER_CREATE

AG_1100

AD00000

01.01.2026

Link

USER_EDIT

AG_1300

AD00002

02.01.2026

Link

USER_DELETE

AG_1400

AD00003

03.01.2026

Link

4.4. Detail View

Mock how the detail view could look like:

NEW: Details are now an inline collapsible section (no separate view or modal required)

Mock detail view
Typ Related entity Creator Field name old new

USER_EDIT

AG_1300

AD00002

Name

Muster

Mustermann

Vorname

Herbert

Fritz

Address1

Musterstrasse

Musterstrasse1

Address2

Musterstrasse

Musterstrasse1

Address3

Musterstrasse

Musterstrasse1

active

false

true

notification

false

true

Role

EMPLOYER

ADMIN

Export/Print (export)

4.5. Erweiterung Audit Scope

In Zukunft ist angedacht in dieser View neben dem Benutzer auch andere Entitäten verwalten/auditieren zu können, bspw. wer hat wann welche Einstellung geändert?

  • Settings / Einstellungen

  • CompanyDaten (Schwellenwerte, …​)

  • DocumentTypes

  • News

  • Mandanten-/Firmendaten

  • DokumentenVerteilung

  • Jobs (Zeitsteuerung, de-/aktiv)

  • Textverwaltung

  • Mutationen (Meta Ebene, z.B Statusänderung)

4.6. Features

4.6.1. Critical activities

Severity: NORMAL, HIGH, VERY HIGH

Möglichkeit der Markierung von "kritisches Events", Typ/Entity-basiert. Bspw.

  • Typ "USER_EDIT", Rollenänderung -> resultiert in HIGH

  • Typ "USER_EDIT", Rollenänderung minor -> higher (EMPLOYER -> ADMIN) -> resultiert in VERY HIGH

4.7. Technischer Lösungsansatz

4.7.1. Anforderung VZ

  • Filter: Im Suchtext soll nach Related Entity (z.B AG_1300) gesucht werden können (nicht nur nach Creator)

  • Tracking von Feld "zugewiesener Arbeitgeber" muss möglich sein

  • Tracking von Feld "Vier-Augen-Kontrolle" muss möglich sein

4.7.2. Diverses

Bitte beachten, dass die Statistiks im Benutzer nicht audtiert werden! Ansonsten führen z.B Logins jeweils zu einer Tabelleneintrag, das ist nicht gewünscht.

4.7.3. Generierung AcitivityItems

Generate activity item

4.7.4. Neue View 'ActivityView'

Enthält eine Liste von Activities inklusive ActivityType (true/false auf ActivityType)-

Definition ActivityType’s

  • UserAdd

  • UserEdit

  • UserDelete

  • UserActivate

  • UserDeActivate

  • …​

4.7.5. Neue Entität ActivityItem

  • Schlanke Persistierung von Wertänderungen. Z.B. Spalten:

  • property

  • beforeValue

  • afterValue

Relevante Properties werden auf DAO Klassen mittels Annotation markiert (z.B. ActivityAudit)

Persistenz Code wird dahingehend erweitert, dass Entitäten für welche Activity Logging konfiguriert ist (via ActivityType) bei add/edit/delete entsprechende Activities persistiert werden.

Mit jeder Persistierung (obiger Punkt) werden die relevanten Property changes ebenfalls persistiert (ActivityItem’s)

5. Audit History Feature - Implementation Summary

Ticket: #7724 Branch: feature/7724-activity-view Status: Core implementation complete, UI styling refinements in progress


5.1. Background & Purpose

This feature adds comprehensive audit logging for administrative entities in pkONE, starting with the User entity. The goal is to provide better traceability of changes for audit compliance.

5.1.1. Why Manual Auditing (Not Hibernate Envers)?

The decision was made to use manual event-based auditing because:

  • User/settings changes are intentional admin actions, not high-frequency operations

  • Need to capture context: WHO made the change and on WHICH entity

  • Better alignment with existing mutation/mutationdata philosophy in pkONE

  • Explicit, understandable code without framework magic

  • More control over what gets audited and how

5.1.2. Activity Types Tracked

Activity Type Description

CREATE

New entity created

EDIT

Entity data modified

DELETE

Entity deleted

ACTIVATE

Entity activated

DEACTIVATE

Entity deactivated


5.2. Architecture Overview

┌─────────────────────────────────────────────────────────────────┐
│                        UI Layer (Oman)                          │
│  ┌─────────────────┐  ┌──────────────────────┐                 │
│  │ ActivityList    │  │ ActivityDetailPanel  │                 │
│  │ (main list view)│  │ (expandable details) │                 │
│  └────────┬────────┘  └──────────┬───────────┘                 │
└───────────┼─────────────────────┼───────────────────────────────┘
            │                      │
┌───────────┼─────────────────────┼───────────────────────────────┐
│           ▼    Service Layer (PkOneBusinessLayer)               │
│  ┌─────────────────────────────────────────────┐               │
│  │              AuditService                    │               │
│  │  - saveWithAudit() / mergeWithAudit()       │               │
│  │  - logCreate/Edit/Delete/Activate/Deactivate│               │
│  │  - Annotation-based property detection      │               │
│  └────────────────────┬────────────────────────┘               │
│                       │                                         │
│  ┌────────────────────▼────────────────────────┐               │
│  │           AuditActivityDao                   │               │
│  │  - findByEntity()                           │               │
│  │  - search with filters                      │               │
│  └─────────────────────────────────────────────┘               │
└─────────────────────────────────────────────────────────────────┘
            │
┌───────────┼─────────────────────────────────────────────────────┐
│           ▼      Domain Layer (AdminDomain)                     │
│  ┌─────────────────┐  ┌───────────────────┐                    │
│  │  AuditActivity  │  │ AuditActivityItem │                    │
│  │  - id           │  │ - id              │                    │
│  │  - actionType   │  │ - propertyName    │                    │
│  │  - entityType   │  │ - beforeValue     │                    │
│  │  - entityId     │  │ - afterValue      │                    │
│  │  - entityName   │  │ - activity (FK)   │                    │
│  │  - creator      │  └───────────────────┘                    │
│  │  - created      │                                           │
│  │  - items (1:N)  │  ┌───────────────────┐                    │
│  └─────────────────┘  │  @Audited         │ Annotations        │
│                       │  @AuditedProperty │                    │
│  ┌─────────────────┐  └───────────────────┘                    │
│  │ AuditActionType │                                           │
│  │ (enum)          │                                           │
│  └─────────────────┘                                           │
└─────────────────────────────────────────────────────────────────┘

5.3. Implementation Details

5.3.1. Domain Entities (AdminDomain)

Location: AdminDomain/src/main/java/ch/optimizeit/domain/admin/audit/

File Purpose

AuditActivity.java

Main audit record entity (JPA)

AuditActivityItem.java

Field-level change details (JPA)

AuditActionType.java

Enum: CREATE, EDIT, DELETE, ACTIVATE, DEACTIVATE

@Audited

Class-level annotation to mark auditable entities

@AuditedProperty

Method-level annotation to mark auditable properties

Database Tables:

  • audit_activity - Main audit log table

  • audit_activity_item - Property changes (linked to activity)

5.3.2. Service Layer (PkOneBusinessLayer)

AuditService Interface (PkOneBusinessLayerApi/…​/audit/AuditService.java):

public interface AuditService {
    void logCreate(Object entity, String entityName, int creatorId);
    void logEdit(Object oldEntity, Object newEntity, String entityName, int creatorId);
    void logDelete(Object entity, String entityName, int creatorId);
    void logActivate(Object entity, String entityName, int creatorId);
    void logDeactivate(Object entity, String entityName, int creatorId);

    // Convenience methods that handle load-compare-save pattern
    <T> void saveWithAudit(T entity, int entityId, String entityName, int creatorId,
                           Function<Integer, T> loader, Consumer<T> detacher, Consumer<T> saver);
    <T> void mergeWithAudit(T entity, int entityId, String entityName, int creatorId,
                            Function<Integer, T> loader, Consumer<T> detacher, Consumer<T> merger);
}

AuditServiceImpl (PkOneBusinessLayer/…​/audit/AuditServiceImpl.java):

  • Uses reflection to detect @Audited and @AuditedProperty annotations

  • Compares old vs new values using annotated getters

  • Only creates EDIT activity if actual changes detected

  • Persists audit records via AuditActivityDao

AuditActivityDao (PkOneBusinessLayerApi/…​/data/AuditActivityDao.java):

  • findByEntity(String entityType, int entityId) - Find all activities for an entity

  • Standard CRUD operations

5.3.3. Integration with OmanUserService

OmanUserServiceImpl (PkOneBusinessLayer/…​/OmanUserServiceImpl.java) integrates with AuditService:

@Autowired(required = false)
private AuditService auditService;

@Override
public void save(OmanUser o) {
    if (this.auditService != null) {
        this.auditService.saveWithAudit(o, o.getId(), o.getUserName(),
                o.getId() > 0 ? o.getModifier() : o.getCreator(),
                omanUserDao::daoLoad, omanUserDao::detach, omanUserDao::daoSave);
    } else {
        omanUserDao.daoSave(o);
    }
}

@Override
public String activateUser(OmanUser user) {
    // ... activation logic ...
    if (this.auditService != null) {
        this.auditService.logActivate(user, user.getUserName(), user.getModifier());
    }
    return password;
}

@Override
public void deactivateUser(OmanUser user, int modifierId) {
    // ... deactivation logic ...
    if (this.auditService != null) {
        this.auditService.logDeactivate(user, user.getUserName(), modifierId);
    }
}

5.3.4. Entity Annotations (OmanUser)

OmanUser (BusinessDomain/…​/entities/OmanUser.java) is annotated for auditing:

@Audited("User")
public class OmanUser extends User {

    @AuditedProperty("firstName")
    public String getFirstName() { ... }

    @AuditedProperty("lastName")
    public String getLastName() { ... }
}

5.3.5. UI Layer (Oman)

Location: Oman/src/main/java/ch/optimizeit/pkone/ui/audit/

File Purpose

ActivityList.java

Main Wicket page with search/filter form and DataView

ActivityList.html

HTML template for list view

ActivityDetailPanel.java

Expandable panel showing property changes

ActivityDetailPanel.html

HTML template for detail view

Features:

  • Search by entity name

  • Filter by action type (dropdown)

  • Paginated list view

  • Expandable inline detail rows showing before/after values

  • Accessible via Menu → "Audit History" (Admin role only)

5.3.6. Spring Configuration

applicationContext-services.xml - AuditService bean wiring:

<bean id="AuditService" class="ch.optimizeit.pkone.services.audit.AuditServiceImpl">
    <property name="auditActivityDao" ref="AuditActivityDao"/>
</bean>

5.3.7. Properties/i18n

application_de.properties (and other locales):

property.audit.tableHeader=Aktivitäten
property.audit.column.timestamp=Timestamp
property.audit.column.type=Aktion
property.audit.column.entityType=Entitätstyp
property.audit.column.entityName=Name
property.audit.column.user=User
property.audit.detail.attribute=Attribut
property.audit.detail.old=Alt
property.audit.detail.new=Neu
property.audit.action.create=Erstellt
property.audit.action.edit=Bearbeitet
property.audit.action.delete=Gelöscht
property.audit.action.activate=Aktiviert
property.audit.action.deactivate=Deaktiviert

5.4. Test Coverage

5.4.1. Unit Tests (in separate branch)

AuditServiceTest (13 tests):

  • testAuditActionTypePropertyKeys - Verify enum property keys

  • testLogCreateWithAuditedAnnotation - Annotation detection for @Audited

  • testLogCreateWithoutAuditedAnnotation - Fallback to class name

  • testLogEditDetectsChanges - Change detection with @AuditedProperty

  • testLogEditNoChangesDoesNotSave - No audit when no changes

  • testLogEditWithNullOldValue - Handle null → value transitions

  • testLogEditWithNullNewValue - Handle value → null transitions

  • testLogDelete - DELETE action logging

  • testLogActivate - ACTIVATE action logging

  • testLogDeactivate - DEACTIVATE action logging

  • testSaveWithAuditNewEntity - CREATE via saveWithAudit

  • testSaveWithAuditExistingEntity - EDIT via saveWithAudit

  • testMergeWithAudit - EDIT via mergeWithAudit

UserAuditTest (12 tests):

  • testOmanUserHasAuditedAnnotation - Verify @Audited("User")

  • testOmanUserFirstNameHasAuditedPropertyAnnotation - Verify @AuditedProperty

  • testOmanUserLastNameHasAuditedPropertyAnnotation - Verify @AuditedProperty

  • testSaveNewUserCallsAuditServiceSaveWithAudit - Integration test

  • testSaveExistingUserCallsAuditServiceSaveWithAudit - Integration test

  • testSaveUserWithoutAuditServiceFallsBackToDao - Graceful degradation

  • testMergeUserCallsAuditServiceMergeWithAudit - Integration test

  • testMergeUserWithoutAuditServiceFallsBackToDao - Graceful degradation

  • testActivateUserCallsAuditServiceLogActivate - Activation audit

  • testActivateUserWithoutAuditServiceDoesNotFail - Graceful degradation

  • testDeactivateUserCallsAuditServiceLogDeactivate - Deactivation audit

  • testDeactivateUserWithoutAuditServiceDoesNotFail - Graceful degradation

Total: 25 tests, 100% passing


5.5. UI Styling Status

5.5.1. Recent Changes (pending application restart)

The UI styling has been updated to match the existing pkONE table styling (e.g., Users list):

ActivityList.html changes:

  • Changed from Bootstrap table table-striped table-hover to pkONE class="list"

  • Removed bordered button styling from expand toggle

  • Simplified expand icon (just count + chevron, no border box)

ActivityDetailPanel.html changes:

  • Changed from Bootstrap bordered table to pkONE class="list"

  • Subtle background for detail section

  • Old values shown in gray, new values slightly bold

Note: Application restart required for HTML changes to take effect.


5.6. Database Schema

CREATE TABLE audit_activity (
                                id INT AUTO_INCREMENT PRIMARY KEY,
                                action_type VARCHAR(20) NOT NULL,
                                entity_type VARCHAR(100) NOT NULL,
                                entity_id INT NOT NULL,
                                entity_name VARCHAR(255),
                                creator INT NOT NULL,
                                created DATETIME NOT NULL,
                                INDEX idx_entity (entity_type, entity_id),
                                INDEX idx_created (created)
);

CREATE TABLE audit_activity_item (
                                     id INT AUTO_INCREMENT PRIMARY KEY,
                                     activity_id INT NOT NULL,
                                     property_name VARCHAR(100) NOT NULL,
                                     before_value TEXT,
                                     after_value TEXT,
                                     FOREIGN KEY (activity_id) REFERENCES audit_activity(id)
);

5.7. Future Extensibility

The architecture supports auditing additional entities by:

  1. Adding @Audited("EntityName") annotation to the entity class

  2. Adding @AuditedProperty("propertyName") to auditable getters

  3. Injecting AuditService into the entity’s service class

  4. Calling appropriate audit methods (saveWithAudit, logActivate, etc.)

Candidates for future auditing:

  • Settings/Configuration changes

  • Company data

  • DocumentTypes

  • News entries

  • Scheduled Jobs configuration

  • Text management

  • Mutation status changes (meta-level)

5.7.1. Critical Activities Feature (Future)

Severity classification for certain events:

  • NORMAL, HIGH, VERY HIGH

  • Example: Role change EMPLOYER → ADMIN = VERY HIGH

  • Would require additional severity field on AuditActivity


5.8. File Reference

5.8.1. New Files Created

Module Path Description

AdminDomain

…​/audit/AuditActivity.java

Main audit entity

AdminDomain

…​/audit/AuditActivityItem.java

Property change entity

AdminDomain

…​/audit/AuditActionType.java

Action type enum

AdminDomain

…​/audit/Audited.java

Class annotation

AdminDomain

…​/audit/AuditedProperty.java

Property annotation

PkOneBusinessLayerApi

…​/audit/AuditService.java

Service interface

PkOneBusinessLayerApi

…​/data/AuditActivityDao.java

DAO interface

PkOneBusinessLayer

…​/audit/AuditServiceImpl.java

Service implementation

PkOneBusinessLayer

…​/data/AuditActivityDaoImpl.java

DAO implementation

Oman

…​/ui/audit/ActivityList.java

List page

Oman

…​/ui/audit/ActivityList.html

List template

Oman

…​/ui/audit/ActivityDetailPanel.java

Detail panel

Oman

…​/ui/audit/ActivityDetailPanel.html

Detail template

5.8.2. Modified Files

Module Path Change

BusinessDomain

…​/entities/OmanUser.java

Added @Audited, @AuditedProperty

PkOneBusinessLayer

…​/OmanUserServiceImpl.java

Integrated AuditService calls

Oman

applicationContext-services.xml

Added AuditService bean

Oman

application_de.properties

Added i18n keys

Oman

Menu configuration

Added "Audit History" menu item


5.9. How to Test Manually

  1. Start the application with VZ profile: gradle :Oman:bootRun -Pargs=--spring.profiles.active=vz,local

  2. Log in as admin user

  3. Navigate to Menu → Benutzer (Users)

  4. Edit a user (change first name or last name)

  5. Navigate to Menu → Audit History

  6. Verify the edit appears in the list

  7. Click the expand button to see before/after values


5.10. Known Issues / TODOs

  • UI styling changes require application restart to take effect

  • Consider adding date range filter to search

  • Consider adding export/print functionality

  • Add more audited properties to OmanUser (email, role, etc.)

  • Extend to other entities (Settings, Company, etc.)

  • Consider critical activities severity classification

6. Expandable Table Row Analysis for Audit History

This document summarizes the technical analysis for implementing expandable table rows in the Audit History feature.

6.1. Current State in pkONE

The codebase currently does not have expandable table rows. Existing patterns include:

  • CollapsiblePanel/CollapsiblePanelDefault - for section-level collapsing (e.g., MutationDetail page with 20+ accordion sections)

  • Modal dialogs - for showing details from table rows

  • Navigation to detail pages - for full detail views

Key files:

  • Oman/src/main/java/ch/optimizeit/pkone/ui/shared/CollapsiblePanel.java

  • Oman/src/main/java/ch/optimizeit/pkone/ui/shared/CollapsiblePanelDefault.java

Use DataView (not ListView or DataTable) combined with Bootstrap 4.6.1’s collapse plugin.

6.2.1. Why DataView?

Component Data Loading Pagination Large Datasets Row Structure Control

ListView

All items in memory

No built-in support

Not suitable

Full control

DataView

Via IDataProvider

Yes (setItemsPerPage())

Suitable

Full control

DataTable

Via IDataProvider

Yes (built-in toolbar)

Suitable

Limited (column-based)

DataView provides:

  • Database-level pagination via IDataProvider (same as DataTable)

  • Full control over HTML structure (needed for two-row-per-item pattern)

  • Compatibility with PagingNavigator

6.2.2. HTML Template Structure

<table class="table">
  <thead>
    <tr>
      <th>Entity</th>
      <th>Typ</th>
      <th>Related Entity</th>
      <th>Creator</th>
      <th>Date</th>
      <th></th><!-- expand toggle column -->
    </tr>
  </thead>
  <tbody>
    <wicket:container wicket:id="rows">
      <!-- Main data row -->
      <tr wicket:id="mainRow">
        <td wicket:id="entity"></td>
        <td wicket:id="type"></td>
        <td wicket:id="relatedEntity"></td>
        <td wicket:id="creator"></td>
        <td wicket:id="date"></td>
        <td>
          <a wicket:id="expandToggle" data-toggle="collapse"
             aria-expanded="false" class="collapsed">
            <i class="pko-expansion-indicator" aria-hidden="true"></i>
          </a>
        </td>
      </tr>
      <!-- Collapsible detail row -->
      <tr wicket:id="detailRow" class="collapse">
        <td colspan="6">
          <wicket:container wicket:id="detailPanel"/>
        </td>
      </tr>
    </wicket:container>
  </tbody>
</table>

<wicket:container wicket:id="navigator"/>

6.2.3. Java Implementation

public class AuditHistoryDataView extends DataView<AuditHistoryItem> {

    public AuditHistoryDataView(String id, IDataProvider<AuditHistoryItem> provider, int itemsPerPage) {
        super(id, provider, itemsPerPage);
    }

    @Override
    protected void populateItem(Item<AuditHistoryItem> item) {
        AuditHistoryItem audit = item.getModelObject();

        // Main row content
        item.add(new Label("entity", audit.getEntity()));
        item.add(new Label("type", audit.getType()));
        item.add(new Label("relatedEntity", audit.getRelatedEntity()));
        item.add(new Label("creator", audit.getCreator()));
        item.add(new Label("date", formatDate(audit.getDate())));

        // Detail row with collapse - unique ID per row
        String detailId = "detail-" + item.getIndex();
        WebMarkupContainer detailRow = new WebMarkupContainer("detailRow");
        detailRow.setMarkupId(detailId);
        detailRow.add(new AuditDetailPanel("detailPanel", item.getModel()));
        item.add(detailRow);

        // Expand toggle linking to detail row
        WebMarkupContainer toggle = new WebMarkupContainer("expandToggle");
        toggle.add(new AttributeModifier("data-toggle", "collapse"));
        toggle.add(new AttributeModifier("data-target", "#" + detailId));
        toggle.add(new AttributeModifier("aria-controls", detailId));
        item.add(toggle);
    }
}

6.2.4. Page Setup with Pagination

public class AuditHistoryPage extends BasePage {

    @Override
    protected void onInitialize() {
        super.onInitialize();

        IDataProvider<AuditHistoryItem> provider = new AuditHistoryDataProvider();
        AuditHistoryDataView dataView = new AuditHistoryDataView("rows", provider, 20);
        add(dataView);

        add(new PagingNavigator("navigator", dataView));
    }
}

6.3. Key Implementation Considerations

Aspect Recommendation

Unique IDs

Use row index or UUID for detail row IDs (pattern from CollapsiblePanel)

ARIA Accessibility

Include aria-expanded, aria-controls attributes

CSS

Use existing .pko-expansion-indicator class for expand/collapse icon

Lazy Loading

Consider loading detail data only on first expand (AJAX) to reduce initial load

State Persistence

Expanded state resets on pagination - acceptable for audit history use case

Detail Panel

Create reusable AuditDetailPanel showing Attribut/Alt/Neu table

6.4. Detail Panel Structure

The expanded detail should display changed attributes in a sub-table:

<!-- AuditDetailPanel.html -->
<wicket:panel>
  <table class="table table-sm">
    <thead>
      <tr>
        <th wicket:id="labelAttribute">Attribut</th>
        <th wicket:id="labelOld">Alt</th>
        <th wicket:id="labelNew">Neu</th>
      </tr>
    </thead>
    <tbody>
      <tr wicket:id="changes">
        <td wicket:id="property"></td>
        <td wicket:id="oldValue"></td>
        <td wicket:id="newValue"></td>
      </tr>
    </tbody>
  </table>
</wicket:panel>

6.5. Existing Patterns to Reuse

From CollapsiblePanelDefault:

  • Bootstrap data-toggle="collapse" integration

  • .pko-expansion-indicator icon styling

  • ARIA accessibility attributes

From MutationDetail page:

  • Accordion pattern with panel-group pko-collapsible classes

  1. DataTable with custom column - Possible but fights against DataTable’s row generation model

  2. ListView - Not suitable for large datasets (loads all items into memory)

  3. Modal dialog - Works but less intuitive than inline expansion per mockup design

  4. Separate detail page - Adds navigation overhead, not aligned with mockup

6.7. References

7. Hibernate Envers Auditing Analysis for PkOne

Complete dialog discussing the viability and implementation approach for Hibernate Envers as an auditing solution in the PkOne system.


7.1. Session Start: Envers Viability Assessment

User: We’re considering adding auditing to the PkOne database. We’re currently looking at Hibernate Envers. What’s your take of Envers' approach to auditing?

Assistant: I’d give you a pragmatic assessment: Hibernate Envers is solid for audit trails, but has some meaningful trade-offs worth considering for your context.

7.1.1. Strengths

Low code friction. Just @Audited on entities and Envers automatically tracks changes via Hibernate events. Minimal refactoring needed.

Temporal queries. You can ask "what did this policy look like on 2024-06-15?" which is genuinely useful. The fluent API is reasonable.

Mature & battle-tested. It’s been in production since Hibernate 3.x. Integration with Spring Data JPA and your existing stack is straightforward.

7.1.2. Real Concerns for PkOne

Audit table explosion. Envers creates parallel {entity}_AUD tables for every audited entity. With your multi-module architecture and many domain entities, you’re doubling storage immediately. At insurance policy scale, this adds up.

Revision granularity mismatch. Envers groups all changes in a single transaction into one "revision." For your domain, you likely need finer business context—not just "who changed it and when," but why (policy amendment reason, employer request, regulatory update, etc.). Your current mutation/mutationdata system probably captures this better.

Performance overhead at scale. Envers triggers on every insert/update/delete. In high-volume scenarios (batch imports, nightly jobs), the audit logging can noticeably slow transaction throughput.

Your existing system conflict. You already have proprietary auditing for mutations. Mixing Envers (automatic, row-level changes) with your mutation-based tracking (business-event-based) creates two parallel audit trails that diverge conceptually and operationally. This gets messy during incident investigation.

7.1.3. Honest Take

Envers works well for administrative entities (users, settings, permissions)—things where "what changed and when" is sufficient.

Envers is redundant for insurance/mutation core if you already have solid mutation tracking. You’d be auditing the same changes twice, differently.

Better hybrid approach:

  • Keep your current mutation/mutationdata for business domain (insurance policies, documents, workflows)—it captures intent

  • Consider Envers only for admin/config entities where you just need change tracking without business semantics

  • If you need temporal queries on policies, enhance your existing system rather than bolting on Envers

Key question to ask: Does your mutation system already give you the audit trail you need? If so, Envers is probably solving a problem you don’t have.

What’s the main audit gap you’re trying to close—is it missing business context, performance on retrieving historical state, or coverage of entities outside the mutation system?


7.2. Clarification: Scope Definition

User: The gap is exactly in the area of changes to users, user/employer associations, settings, job configurations and similar non strictly business domain entities.

Assistant: Perfect—that clarifies it significantly. For that scope, Envers is actually a very good fit.

7.2.1. Why Envers Works Here

Exact match for the audit need. Users, permissions, settings, and job configurations don’t carry the same business context complexity as policies. You just need: "who changed what, when, to what value." That’s Envers' sweet spot.

No conceptual conflict. These entities aren’t part of your mutation/mutationdata system, so you’re not creating duplicate audit trails. Clean separation.

Storage overhead is manageable. Admin/config tables are typically small. The _AUD tables won’t balloon like they would if you audited the entire policy domain.

Compliance angle. For user access logs and setting changes, having an immutable audit trail is often a regulatory or security requirement. Envers gives you that cleanly.

7.2.2. Practical Implementation for PkOne

Entities to audit:

@Audited
public class User { ... }

@Audited
public class UserRole { ... }

@Audited
public class ScheduledJob { ... }

@Audited
public class ApplicationSettings { ... }

@Audited
public class OmanSettings { ... }

What you get:

  • Full history of user permission changes

  • When settings were modified and by whom

  • Job configuration changes with timestamps

  • Temporal queries: "what were user roles on date X?"

Trade-offs are minimal here:

  • Tiny storage footprint relative to your policy data

  • No performance concern for low-volume admin changes

  • Clean audit trail for security/compliance audits

7.2.3. Integration Consideration

Since you’re on Spring Boot 3.x with Hibernate 6.2, Envers integration is straightforward:

implementation 'org.hibernate:hibernate-envers:6.2.x'

Enable in your Spring context and you’re mostly done. Flyway migrations will handle the _AUD table creation.

7.2.4. One Caveat

Make sure you capture WHO changed things. Envers tracks the revision number and timestamp, but you need to ensure the Spring Security principal is available to record the username. This usually requires a custom RevisionListener:

@RevisionEntity(PkOneRevisionListener.class)
public class PkOneRevision { ... }

Otherwise you’ll get "user changed this" but not which user.

Bottom line: For your stated scope, Envers is the right tool. It solves the problem without added complexity. I’d recommend it.


7.3. Deep Dive: Many-to-Many Relationships

User: How does Envers handle n:m relations like user to employers (via the user_employer entity). Admins can add/remove employers to specific users when editing the user. Such changes would also have to be tracked.

Assistant: Great question—this is where Envers gets tricky and where many projects run into unexpected audit gaps.

7.3.1. Standard @ManyToMany Behavior

Short answer: Envers tracks n:m collection changes, but with important caveats.

If you have:

@Entity
@Audited
public class User {
    @ManyToMany
    @JoinTable(name = "user_employer")
    private Set<Employer> employers;
}

@Entity
@Audited
public class Employer {
    @ManyToMany(mappedBy = "employers")
    private Set<User> users;
}

Envers will create a user_employer_AUD table and track when rows are added/removed. You can query: "What employers did user X have on 2024-01-15?"

7.3.2. The Gotchas

1. Collection initialization matters. If the employers collection isn’t fully loaded when you modify it, Envers may miss changes. This is a real problem in Spring Data scenarios where you lazily fetch collections.

2. The audit trail is collection-level, not granular. You see "employer was added/removed" with a timestamp, but not:

  • Who performed the change (the admin user)

  • Why it was changed (reason/comment)

  • When exactly it was assigned

3. Bidirectional consistency. If admins are directly manipulating the join table or the relationship, you risk corrupting the audit trail if both sides aren’t synchronized properly.

7.3.3. Better Approach: Map the Join Table as an Entity

For user-employer assignments, you likely do want those details. Map it explicitly:

@Entity
@Audited
public class UserEmployer {
    @Id
    private UserEmployerId id;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;

    @ManyToOne
    @JoinColumn(name = "employer_id")
    private Employer employer;

    private LocalDateTime assignedDate;

    @ManyToOne
    @JoinColumn(name = "assigned_by_user_id")
    private User assignedByUser;
}

Then on User:

@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
@Audited
private Set<UserEmployer> userEmployers;

Why this is better:

  • Full audit trail of every assignment change

  • You capture who made the change (reference to the admin user)

  • You know when the assignment occurred

  • Each UserEmployer row change is a discrete audit entry

  • Envers handles it reliably without collection initialization issues

7.3.4. Practical Implementation

Your admin edit form would then do:

UserEmployer assignment = new UserEmployer();
assignment.setUser(user);
assignment.setEmployer(employer);
assignment.setAssignedDate(LocalDateTime.now());
assignment.setAssignedByUser(currentAdminUser);

user.getUserEmployers().add(assignment);
userRepository.save(user);

Envers captures the entire UserEmployer entity change—no ambiguity.

7.3.5. Query Historical State

AuditReader auditReader = AuditReaderFactory.get(entityManager);

// "What employers did user 123 have on date X?"
List<UserEmployer> historicalAssignments = auditReader
    .createQuery()
    .forEntitiesAtRevision(UserEmployer.class, revisionNumber)
    .add(AuditEntity.property("user.id").eq(123L))
    .getResultList();

7.3.6. Bottom Line

For simple add/remove tracking: Standard @ManyToMany with @Audited works, but watch for collection initialization.

For your scenario (admin-managed user-employer assignments): Definitely map the join table as an entity. You’ll want to know who assigned what and when, and that requires capturing more than just the join table’s foreign keys.

This also aligns better with your existing mutation/mutationdata philosophy—you’re treating the assignment as a business event with context, not just a relationship change.


7.4. Alternative Approach: Manual Event-Based Auditing

User: If Envers didn’t exist. How would you do this?

Assistant: Excellent reframing. This actually clarifies whether Envers is solving a real problem or just providing convenient automation.

7.4.1. Manual Approach: Event-Based Auditing

Model it like your existing mutation/mutationdata system. Don’t try to auto-track everything—capture auditable events at the business logic layer.

@Entity
public class AdminAuditLog {
    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne
    private User performedByUser;

    private LocalDateTime changedAt;

    private String entityType;      // "User", "UserEmployer", "ApplicationSettings"
    private Long entityId;

    private String action;          // "USER_CREATED", "EMPLOYER_ASSIGNED", "SETTING_CHANGED"
    private String oldValue;        // JSON or structured representation
    private String newValue;
    private String reason;          // optional: why this change
}

Then at the service layer, you explicitly log:

@Service
public class UserEmployerService {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private AdminAuditLogRepository auditLogRepository;

    public void assignEmployerToUser(Long userId, Long employerId, String reason) {
        User user = userRepository.findById(userId).orElseThrow();
        Employer employer = employerRepository.findById(employerId).orElseThrow();

        UserEmployer assignment = new UserEmployer(user, employer);
        user.getUserEmployers().add(assignment);
        userRepository.save(user);

        // Explicit audit capture
        AdminAuditLog log = new AdminAuditLog();
        log.setPerformedByUser(getCurrentUser());
        log.setEntityType("UserEmployer");
        log.setEntityId(assignment.getId());
        log.setAction("EMPLOYER_ASSIGNED");
        log.setNewValue(toJson(assignment));
        log.setReason(reason);
        log.setChangedAt(LocalDateTime.now());

        auditLogRepository.save(log);
    }

    public void removeEmployerFromUser(Long userId, Long employerId, String reason) {
        User user = userRepository.findById(userId).orElseThrow();
        UserEmployer assignment = user.getUserEmployers()
            .stream()
            .filter(a -> a.getEmployer().getId().equals(employerId))
            .findFirst()
            .orElseThrow();

        String oldValue = toJson(assignment);
        user.getUserEmployers().remove(assignment);
        userRepository.save(user);

        AdminAuditLog log = new AdminAuditLog();
        log.setPerformedByUser(getCurrentUser());
        log.setEntityType("UserEmployer");
        log.setEntityId(assignment.getId());
        log.setAction("EMPLOYER_REMOVED");
        log.setOldValue(oldValue);
        log.setReason(reason);
        log.setChangedAt(LocalDateTime.now());

        auditLogRepository.save(log);
    }
}

7.4.2. Why This is Actually Better

1. Intent is explicit. You know why every line of audit code exists. No mystery framework magic.

2. Context is captured naturally. The reason field, the performing user, the exact action—all there because you wrote it.

3. High-volume performance is irrelevant. User/employer assignments aren’t high-frequency. You can afford to explicitly log each one.

4. Query simplicity. Want to know "all employers assigned to user X in the last 90 days?"

auditLogRepository.findByEntityTypeAndEntityIdAndActionAndChangedAtAfter(
    "UserEmployer", userId, "EMPLOYER_ASSIGNED", LocalDateTime.now().minusDays(90)
);

5. Compliance audit trail. For regulatory purposes, you can easily generate reports: "Who assigned this employer to this user and why?"

6. No mysterious side effects. If a bug corrupts your audit log, you wrote the bug—you can see it.

7.4.3. For Settings Changes

@Service
public class SettingsService {
    public void updateApplicationSetting(String key, String oldValue, String newValue, String reason) {
        applicationSettings.put(key, newValue);

        AdminAuditLog log = new AdminAuditLog();
        log.setPerformedByUser(getCurrentUser());
        log.setEntityType("ApplicationSettings");
        log.setEntityId(settingsId);
        log.setAction("SETTING_CHANGED");
        log.setOldValue(oldValue);
        log.setNewValue(newValue);
        log.setReason(reason);
        log.setChangedAt(LocalDateTime.now());

        auditLogRepository.save(log);
    }
}

7.4.4. For User Changes

public void updateUserRoles(Long userId, Set<Role> newRoles, String reason) {
    User user = userRepository.findById(userId).orElseThrow();
    Set<Role> oldRoles = new HashSet<>(user.getRoles());

    user.setRoles(newRoles);
    userRepository.save(user);

    AdminAuditLog log = new AdminAuditLog();
    log.setPerformedByUser(getCurrentUser());
    log.setEntityType("User");
    log.setEntityId(userId);
    log.setAction("ROLES_CHANGED");
    log.setOldValue(toJson(oldRoles));
    log.setNewValue(toJson(newRoles));
    log.setReason(reason);
    log.setChangedAt(LocalDateTime.now());

    auditLogRepository.save(log);
}

7.4.5. The Trade-off Analysis

Aspect Manual Auditing Envers

Code volume

More (explicit logging calls)

Less (annotations)

Performance

Predictable, under your control

Automatic, less visibility

Context capture

Natural (you decide what to log)

Limited (Envers decides)

Query complexity

Simple business queries

Complex temporal queries

Maintenance

Obvious where changes come from

Hidden in framework mechanics

Compliance reporting

Easy (you built it)

Adequate (Envers handles it)

7.4.6. My Assessment

For your use case (user/employer/settings changes)? Manual auditing wins. These are intentional admin actions, not high-frequency system noise. You want to capture intent and context. Writing the code explicitly makes that natural.

Envers would buy you: Less boilerplate, but at the cost of clarity and context.

What I’d actually do: Combine both approaches:

  • Manual auditing for admin/user changes (where context and intent matter)

  • Envers for policy domain read-only temporal queries (if you need "what did this policy look like on date X" without capturing the why)

Or just stick with manual for everything admin-related and save the Envers dependency entirely. The audit logging layer you’d write is ~200 lines of code, one-time investment, and you own it forever.

8. Zugewiesene Arbeitgeber und entsprechende Properties

PVS mit PV/GV PVS ohne PV/GV Reine PV/GV SAM PFS Property Beispieltext

-

-

-

-

1

property.PFS_Kurz

PFS

-

-

-

-

mehr als 1

property.PFS

VZ Personalfürsorge

-

-

1

-

-

property.PVS-PV

VZ Vorsorgelösung für Firmen

-

-

1 oder >1

1 oder >1

-

property.SAM-PV + property.companySubheader2

VZ Vorsorgelösung für Firmen und Zusatzvorsorge (VZ Sammelstiftung)

-

1

-

-

-

property.PVS

VZ Vorsorgelösung für Firmen VZ BVG Sammelstiftung)

-

-

-

1

-

-

property.SAM

Zusatzvorsorge (VZ Sammelstiftung)

1

-

1

property.PVS-SAM + property.companySubheader

VZ Vorsorgelösungen (VZ BVG Sammelstiftung & VZ Sammelstiftung)

1

1

property.PVS-PV

VZ Vorsorgelösungen

1

1

property.PVS-SAM + property.companySubheader

VZ Vorsorgelösungen (VZ BVG Sammelstiftung & VZ Sammelstiftung)

Reine PV/GV --> BSOHNEBVG = 0 / EXBVG = 0 Konstellation SAM mit PV/GV = Immer zwei Mandantennummer, aber mit gleichem Firmennamen Grund: PV/GV ist IMMER Company PVS

9. Ticket #7844 - Solution Overview

9.1. Problem

Focus generates Kontokorrent (current account statement) documents for all VZ employers, including those with a zero balance. Some employers find zero-balance Kontokorrente useful, others are annoyed by receiving notifications for documents that require no action.

9.2. Solution

9.2.1. New DocumentType for zero-balance Kontokorrente

A dedicated DocumentType (e.g. KONTOKORRENT_SALDO0) is introduced alongside the existing KONTOKORRENT type. It represents invoices/statements that require no action from the employer.

9.2.2. Detection during document distribution

The existing document distribution pipeline uses a filename convention to distinguish the two cases. Focus delivers zero-balance Kontokorrente with a _Saldo0 suffix in the filename:

  • Regular: Kontokorrent_erstellt am 03.02.2026_2025_93028_DE.pdf

  • Zero balance: Kontokorrent_erstellt am 03.02.2026_2025_93028_DE_Saldo0.pdf

A second DocumentDistributionDefinition with a matching regex routes _Saldo0 files to the new DocumentType. Documents are imported and distributed to the employer as usual — nothing is suppressed or hidden.

9.2.3. Notification preferences per DocumentType

A new screen in the employer/webcustomer portal lets users configure which DocumentTypes should trigger notifications. Employers who don’t want to be notified about zero-balance Kontokorrente simply disable notifications for that DocumentType.

9.3. Key characteristics

  • No data hidden: All documents remain visible in the document archive regardless of notification preferences.

  • Control at notification level: The preference controls whether a notification is sent, not whether the document is distributed.

  • Generic mechanism: Per-DocumentType notification preferences are reusable for any document type, not just Kontokorrente.

  • Clean boundary: The filename convention is only used at the import boundary to select the right DocumentType. After import, the system works entirely on DocumentType metadata.

9.4. Optional DocumentStore Extensions

9.4.1. New column 'Dokument-Typ'

We’ll introduce a new column showing a document’s document type.

9.4.2. New filter 'Dokument-Typ'

A new filter field will allow the user to filter for any of the configured document types.

9.4.3. Optional Per-User Filter Pre-Sets

The new preferences screen (point 3, above) will also allow the user to set filter pre-sets on the level of the document type. This way, the user has the option to exclude certain document types from showing when he/she navigates to the DocumentStore. Once in the document store, the filter pre-sets can be overridden (e.g. by clearing the filter pre-set and thus showing all document types)

9.5. Effort Estimation

9.5.1. A) New DocumentType + DocumentDistributionDefinition

New DocumentType KONTOKORRENT_SALDO0 in DocumentTypeProviderVz, plus a second DDD with a regex matching the _Saldo0 filename suffix. Straightforward configuration work following established patterns.

9.5.2. B) Notification preferences per DocumentType

New entity (e.g. EmployerNotificationPreference) with DB migration, DAO/repository, and service layer. New Wicket page/panel in the webcustomer portal for managing preferences per DocumentType. Integration into the notification pipeline (DocumentNotificationEventListener) to check preferences before dispatching. This is the main work item — no existing webcustomer settings/preferences screen exists to build on.

Abzuklären: lösen wir dies mittels Settings? Existiert ein Konzept von per-user settings?

9.5.3. C) DocumentStore: new column and filter

Add a "Dokument-Typ" column to DocumentStorePanel and a corresponding dropdown filter. The Document entity already has a documentType reference. Requires extending DocumentMetaDataProvider filtering logic.

9.5.4. D) Per-user filter pre-sets (optional)

Extend the preferences screen (B) to also allow setting default DocumentType filters for the DocumentStore view. On navigation to DocumentStore, apply these pre-sets as initial filter state. User can override in-session.