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.
2.2.2. Einführung von Links in Notifizierungen (#7707)
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.
-
-
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.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.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.
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.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 |
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 how the list view could look like:
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)
| 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.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.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/mutationdataphilosophy in pkONE -
Explicit, understandable code without framework magic
-
More control over what gets audited and how
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 |
|---|---|
|
Main audit record entity (JPA) |
|
Field-level change details (JPA) |
|
Enum: CREATE, EDIT, DELETE, ACTIVATE, DEACTIVATE |
|
Class-level annotation to mark auditable entities |
|
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
@Auditedand@AuditedPropertyannotations -
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 |
|---|---|
|
Main Wicket page with search/filter form and DataView |
|
HTML template for list view |
|
Expandable panel showing property changes |
|
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-hoverto pkONEclass="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:
-
Adding
@Audited("EntityName")annotation to the entity class -
Adding
@AuditedProperty("propertyName")to auditable getters -
Injecting
AuditServiceinto the entity’s service class -
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.8. File Reference
5.8.1. New Files Created
| Module | Path | Description |
|---|---|---|
AdminDomain |
|
Main audit entity |
AdminDomain |
|
Property change entity |
AdminDomain |
|
Action type enum |
AdminDomain |
|
Class annotation |
AdminDomain |
|
Property annotation |
PkOneBusinessLayerApi |
|
Service interface |
PkOneBusinessLayerApi |
|
DAO interface |
PkOneBusinessLayer |
|
Service implementation |
PkOneBusinessLayer |
|
DAO implementation |
Oman |
|
List page |
Oman |
|
List template |
Oman |
|
Detail panel |
Oman |
|
Detail template |
5.8.2. Modified Files
| Module | Path | Change |
|---|---|---|
BusinessDomain |
|
Added @Audited, @AuditedProperty |
PkOneBusinessLayer |
|
Integrated AuditService calls |
Oman |
|
Added AuditService bean |
Oman |
|
Added i18n keys |
Oman |
Menu configuration |
Added "Audit History" menu item |
5.9. How to Test Manually
-
Start the application with VZ profile:
gradle :Oman:bootRun -Pargs=--spring.profiles.active=vz,local -
Log in as admin user
-
Navigate to Menu → Benutzer (Users)
-
Edit a user (change first name or last name)
-
Navigate to Menu → Audit History
-
Verify the edit appears in the list
-
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
6.2. Recommended Approach: DataView with Bootstrap Collapse
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 |
Yes ( |
Suitable |
Full control |
DataTable |
Via |
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
|
ARIA Accessibility |
Include |
CSS |
Use existing |
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 |
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-indicatoricon styling -
ARIA accessibility attributes
From MutationDetail page:
-
Accordion pattern with
panel-group pko-collapsibleclasses
6.6. Alternative Approaches (Not Recommended)
-
DataTable with custom column - Possible but fights against DataTable’s row generation model
-
ListView - Not suitable for large datasets (loads all items into memory)
-
Modal dialog - Works but less intuitive than inline expansion per mockup design
-
Separate detail page - Adds navigation overhead, not aligned with mockup
6.7. References
-
Mockup:
AuditHistory-Mockup-DetailView.png -
Specification:
auditHistory.md -
Existing collapsible implementation:
CollapsiblePanelDefault.java -
Bootstrap 4 Collapse: https://getbootstrap.com/docs/4.6/components/collapse/
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/mutationdatafor 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
UserEmployerrow 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.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.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?