# Cloudkasten GmbH

## Documentation

# Setting Up Email Sending: The Mail Task (SMTP Configuration)

**Module:** Systemadministration → Hintergrundaufgaben (System Administration → Background Tasks)

**Audience:** Administrators

**Required permission:** `system.background_tasks`

## Overview

FMWeb sends emails (e.g. HU (statutory vehicle inspection) reminders, lease warnings, reservation confirmations) in two stages:

1. A daily background service checks, on the configured weekday, which notifications are due and places them into a **send queue**.
2. The **Mail Task** empties this queue at regular intervals and sends the emails via your SMTP server.

This article describes the **global SMTP configuration** (transport). The content settings — *which* notifications are sent — are configured separately per client (Mandant); see article [02 – Email settings per client](https://support.cloudkasten.net/en-US/kb/article/2).

> **Important:** Both configurations are required. Without an active Mail Task, all notifications stay in the queue and are never delivered.

## Step by step

1. Open **Systemadministration → Hintergrundaufgaben** (`/applications/backgroundtasks/dashboard`).
2. Switch to the **„E-Mail Task"** (Email Task) tab.
3. Fill in the fields:

| Field | Meaning |
| --- | --- |
| **Ist aktiv?** (Is active?) | Turns sending on/off. |
| **Wartezeit zwischen den Ausführungen** (Wait time between runs) | Interval in minutes at which the queue is emptied (recommended: 5). |
| **SMTP Server** | Hostname of your mail server, e.g. `smtp.ihrefirma.de`. |
| **SMTP Benutzer** (SMTP user) | Login name for the mail server. **This address also appears as the sender** of the emails. |
| **SMTP Passwort** (SMTP password) | Password of the SMTP user. |
| **SMTP Port** | Typical: 587 (STARTTLS) or 465/25 – depending on your mail server. |
| **SSL aktivieren?** (Enable SSL?) | Encrypted connection to the mail server (recommended). |
| **Domäne (nur für Exchange)** (Domain (Exchange only)) | Only fill this in if your Exchange server requires a domain for login. |
| **Mandanten-Auswahl** (Client selection) | Select **every client** whose emails should be sent. Clients that are not selected stay in the queue! |

1. Click **Speichern** (Save).

## How it works & notes

- The sender address of the emails that are sent is the **SMTP Benutzer** (SMTP user). The sender chosen under „E-Mail Einstellungen" (Email Settings) — the fleet manager — appears in the template placeholders (signature, contact details), not in the technical sender field.
- There is currently **no „Testmail senden" (Send test mail) button**. After setup, verify delivery via **Protokolle → E-Mail Überwachung** (Logs → Email Monitoring) (see article [07 – Email troubleshooting](https://support.cloudkasten.net/en-US/kb/article/789)).
- Failed send attempts are marked with the status „Fehlgeschlagen" (Failed) and logged in the Logs tab of Hintergrundaufgaben (Background Tasks).

## Common problems

| Symptom | Cause / Solution |
| --- | --- |
| No email arrives | „Ist aktiv?" (Is active?) is not set, or the client is missing from the Mandanten-Auswahl (client selection). |
| Emails land in spam | Set up SPF/DKIM for the sender address (SMTP user) at your mail provider. |
| Authentication error | Check user/password; for Microsoft 365, enable an app password or SMTP auth if necessary. |
| Only one client receives mail | Add the other clients to the Mandanten-Auswahl (client selection) and save. |

## Related articles

- [02 – Email settings per client](https://support.cloudkasten.net/en-US/kb/article/2)
- [03 – Email templates and placeholders](https://support.cloudkasten.net/en-US/kb/article/785)
- [07 – Email troubleshooting](https://support.cloudkasten.net/en-US/kb/article/789)

# Email Settings: Enabling Info Mails per Client

**Module:** Systemadministration → E-Mail Einstellungen (System Administration → Email Settings)

**Audience:** Administrators, fleet managers

**Required permission:** `system.mail_settings`

## Overview

While the Mail Task (see article [01](https://support.cloudkasten.net/en-US/kb/article/1)) handles the technical sending, under **E-Mail Einstellungen** (Email Settings) you define, per client (Mandant), **which** notifications are generated: HU/UVV reminders, tyre changes, driving-licence checks, lease warnings and reports.

You will find the page under **Systemadministration → E-Mail Einstellungen** (`/applications/settings/mailSettings`). All settings apply **only to the currently logged-in client**.

## The „Allgemein" (General) tab – the basic switches

| Setting | Meaning |
| --- | --- |
| **Infomails sollen versendet werden?** (Send info mails?) | Master switch. If it is off, **no** automatic notification is generated for this client – regardless of all other settings. |
| **Absenderinformationen auswählen** (Select sender information) | Choose a user with the fleet-manager flag. Their name, email and phone appear in the template placeholders (`{{ fleet_manager.* }}`) – e.g. in the signature. |
| **Versand an folgendem Wochentag durchführen** (Send on the following weekday) | The automatic notifications are generated **once a week** on the chosen weekday (default: Thursday). |

> **Good to know:** The notification service runs once a day. However, it only generates info mails if the current day matches the configured weekday. Changes to the settings therefore only take effect on the next configured send day.

## The other tabs

- **Wartung** (Maintenance) – HU and UVV/DGUV reminders → article [04](https://support.cloudkasten.net/en-US/kb/article/786)
- **Reifenwechsel** (Tyre change) – seasonal tyre-change reminders → article [05](https://support.cloudkasten.net/en-US/kb/article/787)
- **Führerscheine** (Driving licences) – reminders for driving-licence checks → article [05](https://support.cloudkasten.net/en-US/kb/article/787)
- **Poolfahrzeugdisposition** (Pool-vehicle scheduling) – email sending for reservations from the Fahrerportal (driver portal) (template + activation)
- **Leasing** (Leasing) – warnings about expiring, over-mileage and under-mileage lease contracts → article [06](https://support.cloudkasten.net/en-US/kb/article/788)
- **Berichte** (Reports) – the consolidated lease mileage projection report → article [06](https://support.cloudkasten.net/en-US/kb/article/788)

The same pattern applies to every notification type:

1. **Enable** (tick the checkbox)
2. Set the **Vorlaufzeit** (lead time) in days (how many days before the due date the recipient is informed)
3. **Select an email template** (without an assigned template, no mail is generated → article [03](https://support.cloudkasten.net/en-US/kb/article/785))

## Checklist: to make sure mails really arrive

1. ✅ Mail Task active and the client included in the Mandanten-Auswahl (client selection) (article [01](https://support.cloudkasten.net/en-US/kb/article/1))
2. ✅ „Infomails sollen versendet werden?" (Send info mails?) enabled
3. ✅ Sender selected
4. ✅ Desired notification type enabled **and** a template assigned
5. ✅ Recipients maintained (drivers with a valid email address, without a block)

## Related articles

- [01 – Setting up email sending (Mail Task)](https://support.cloudkasten.net/en-US/kb/article/1)
- [03 – Email templates and placeholders](https://support.cloudkasten.net/en-US/kb/article/785)
- [07 – Email troubleshooting](https://support.cloudkasten.net/en-US/kb/article/789)

# Account lockout: what it is and how to recover

To protect accounts against password guessing, MobilityManager temporarily locks an account after repeated failed sign-in attempts. This article explains when a lockout happens and how to recover.

## How lockout works

- After **5 failed sign-in attempts**, your account is locked.
- The lockout lasts **15 minutes**.
- While locked, every sign-in attempt is refused, even with the correct password.

When your account is locked you see a message such as "Account is locked. Try again in N minute(s)." The number counts down as the lockout period passes.

> **Note:** Each wrong password increases a failed-attempt counter. A single successful sign-in resets that counter to zero, so occasional mistakes do not accumulate over days.

## How to recover

### Option 1: Wait for the lockout to expire

The simplest option is to wait. After 15 minutes the lockout clears automatically, the failed-attempt counter is reset, and you can sign in again with the correct password.

### Option 2: Reset your password to unlock immediately

If you cannot wait, or you are locked out because you forgot your password, reset it:

1. On the sign-in page, choose **Forgot password**.
2. Request a reset for your email address.
3. Set a new password using the reset token.

Completing a password reset clears the lockout and resets the failed-attempt count, so you can sign in straight away with your new password.

> **Tip:** Resetting your password is the fastest way out of a lockout. It removes the lockout the moment the new password is saved, without waiting the remaining minutes.

## Avoiding lockouts

- If you are unsure of your password, reset it rather than guessing repeatedly, since each wrong try counts toward the 5-attempt limit.
- Watch for auto-filled credentials from an old password saved in your browser, which can silently use up attempts.
- Check that you selected the correct company before you sign in.

## When to contact an administrator

| Situation | What to do |
| --- | --- |
| You keep getting locked out even with the right password | Reset your password to be sure, then contact your administrator if it recurs. |
| You cannot receive the reset token | Ask your administrator to help with the reset. |
| You suspect someone is trying to access your account | Reset your password and notify your administrator. |

## Related

- Resetting your password
- Password requirements
- Signing in with your email and password
- Signing out of MobilityManager

# Password requirements

This article describes the password rules MobilityManager enforces, so you can choose a password that will be accepted the first time.

## The enforced rule

MobilityManager enforces a single password rule everywhere a password is set:

- Your password must be at least **8 characters** long.
- It cannot be blank or consist only of spaces.

If a password is shorter than 8 characters, you see: "Password must be at least 8 characters long." Enter a longer password to continue.

> **Note:** The 8-character minimum is the only rule the system requires. There is no enforced requirement for uppercase letters, numbers, or symbols. That does not mean a short, simple password is a good idea, only that the platform will not reject it on those grounds.

## Where the rule applies

The same minimum-length check runs at every point where a password is set, so the rule cannot vary between different screens:

| Situation | Who does it |
| --- | --- |
| Creating a new user account | SystemAdmin or FleetManager |
| Administrator resetting another user's password | SystemAdmin or FleetManager |
| Self-service password reset | Any user with a local password |

## Recommended practice

Although only length is enforced, a strong password protects your account and your company's fleet data. We recommend that you:

- Use a passphrase of several unrelated words, which is easy to remember and hard to guess.
- Mix in uppercase letters, numbers, and symbols where you can.
- Avoid reusing a password from another service.
- Use a password manager to generate and store a long, unique password.

> **Tip:** Because repeated wrong entries lock your account for 15 minutes after 5 failed attempts, a password you can remember reliably is worth choosing. If you do get locked out, a password reset clears the lockout immediately.

## Single sign-on accounts

If you sign in through single sign-on (for example Azure AD or Keycloak), your password is managed by your identity provider, not by MobilityManager. The 8-character rule described here applies only to local passwords held in MobilityManager. For SSO accounts, follow your provider's password policy.

## Related

- Resetting your password
- Account lockout: what it is and how to recover
- Signing in with your email and password
- Single sign-on: an overview

# Resetting your password

This article walks you through resetting your MobilityManager password when you have forgotten it or your account is locked. The reset uses a one-time token that is valid for 24 hours.

## Step 1: Request a password reset

1. On the sign-in page, choose **Forgot password**.
2. Enter the email address for your account.
3. Submit the request.

You always see the same confirmation: "If an account with that email exists, a password reset has been initiated." This message appears whether or not the email is registered.

> **Note:** The neutral confirmation is a security measure. MobilityManager never reveals whether an email address belongs to a real account, so no one can use the reset form to discover valid accounts.

## Step 2: Get your reset token

When your email matches an active account, MobilityManager generates a secure, single-use reset token and stores it with a 24-hour expiry. The token is delivered through the channel your organization has configured. In most deployments this is an email to your address. In on-premises deployments, an administrator may retrieve and pass the token to you directly.

> **Warning:** The reset token is never shown in the browser response and is never returned by the reset form itself. Treat the token like a password. Do not share it, and use it only on the official reset page.

## Step 3: Set a new password

1. Open the reset page (from your email link or as directed by your administrator).
2. Enter your email address.
3. Enter the reset token.
4. Enter your new password, then confirm it. Both entries must match.
5. Submit to complete the reset.

Your new password must meet the password policy. When the reset succeeds, MobilityManager clears the reset token, resets your failed-attempt count to zero, and removes any active lockout, so you can sign in right away.

## Common problems

| Message | Cause | What to do |
| --- | --- | --- |
| Passwords do not match | The new password and its confirmation differ. | Re-enter both fields identically. |
| Password must be at least 8 characters long | The new password is too short or blank. | Choose a password of at least 8 characters. |
| Invalid reset token | The token does not match the account, or has already been used. | Request a new reset and use the latest token. |
| Reset token has expired. Please request a new one. | More than 24 hours passed since the request. | Start again from Step 1. |

> **Tip:** If you sign in only through single sign-on, you do not have a local password to reset. Sign in through your SSO provider instead, or ask your administrator.

## Related

- Password requirements
- Account lockout: what it is and how to recover
- Signing in with your email and password
- Single sign-on: an overview

# Choosing your company at login

MobilityManager is a multi-tenant platform, so when you sign in you choose which company (tenant) you are working in. This article explains how the company list works and what your choice affects.

## Why you pick a company

Every user account can be linked to one or more companies. Each company keeps its own fleet, drivers, documents, and settings. Choosing a company at login tells MobilityManager which data set and which of your roles to load for this session.

## Selecting your company

1. Open the sign-in page.
2. Select your company from the list of available companies.
3. Continue with your email and password, or with single sign-on if it is offered for that company.

> **Note:** Only companies with an active status appear in the list. If a company has been deactivated, it is hidden from selection and you cannot sign in to it.

## How your company choice affects the session

- **Roles and permissions:** Your session loads only the roles you hold in the selected company. If you are a FleetManager in one company and a Driver in another, your permissions match whichever company you signed in to.
- **Available sign-in methods:** Each company decides whether single sign-on is offered. When you pick a company, MobilityManager shows the login options configured for it. Email and password sign-in is always available; a single sign-on button appears only when SSO is enabled for that company.
- **Data scope:** You see the vehicles, drivers, and documents belonging to that company only.

## Access rules

You can sign in to a company only if you have a role there. If you select a company where you have no role, sign-in is refused with "User does not have access to this tenant." The exception is the SystemAdmin role, which may access any company.

> **Tip:** If you belong to more than one company, you do not have to sign out to move between them. After signing in, use the company switcher to change companies within the same session. See "Switching between companies without signing out."

## If your company is missing or deactivated

| Situation | What to do |
| --- | --- |
| Your company is not in the list | The company may be deactivated, or you may be on the wrong sign-in page. Contact your administrator to confirm. |
| You see "Tenant has been deactivated" | The company is no longer active. Ask your administrator when access will be restored. |
| You see "User does not have access to this tenant" | You have no role in that company. Ask an administrator to assign you a role there. |

## Related

- Signing in with your email and password
- Switching between companies without signing out
- Single sign-on: an overview
- Signing out of MobilityManager

# Signing in with your email and password

This article shows you how to sign in to MobilityManager with your email address and password, and what to do if sign-in fails.

## Before you begin

- Your account must be active. An administrator (SystemAdmin or FleetManager) creates accounts and can deactivate them.
- You must know which company (tenant) you belong to. MobilityManager is multi-tenant, so your login is tied to a specific company.
- Your account must have a password set. If you sign in only through single sign-on, see the SSO articles instead.

## Sign in step by step

1. Open the MobilityManager sign-in page in your browser.
2. Select your company from the company list. Only active companies appear here.
3. Enter your email address in the email field.
4. Enter your password.
5. Choose **Sign in**.

When your credentials are correct, you are signed in and a session is created for you. Your roles and permissions are loaded for the company you selected.

> **Note:** A sign-in session stays valid for 8 hours. After that you are signed out automatically and need to sign in again.

## What happens behind the scenes

MobilityManager looks up your account by email, confirms it is active, verifies your password, and then issues a secure session token scoped to your selected company. On a successful sign-in the system also records the time as your last login and clears any previous failed-attempt count.

## If sign-in fails

| Message | What it means | What to do |
| --- | --- | --- |
| Invalid email or password | The email, the password, or your account status did not match. For security, the same message is shown whether the email is unknown, the password is wrong, or the account is inactive. | Re-enter your details carefully. If it still fails, reset your password or contact your administrator. |
| Account is locked. Try again in N minute(s). | Too many failed attempts triggered a temporary lockout. | Wait the stated time, or reset your password to clear the lockout immediately. |
| Tenant has been deactivated. Please contact your administrator. | The company you selected is no longer active. | Contact your administrator to confirm your company access. |
| User does not have access to this tenant | Your account has no role in the company you selected. | Choose a company you belong to, or ask an administrator to grant access. |

> **Tip:** The generic "Invalid email or password" message is intentional. MobilityManager never confirms whether an email address exists, which protects your account against guessing attacks.

## Related

- Choosing your company at login
- Resetting your password
- Account lockout: what it is and how to recover
- Signing out of MobilityManager

# Signing out of MobilityManager

This article explains how to sign out of MobilityManager and what happens to your session when you do.

## How to sign out

1. Open your user menu in the top area of the application.
2. Choose **Sign out** (Logout).

You are returned to the sign-in page. Your session token is removed from your browser, so the application can no longer make requests on your behalf from that device.

## How sessions work

MobilityManager uses a stateless session token. Signing out removes that token from your device. Because the token is what proves who you are, removing it ends your access immediately in that browser.

> **Note:** Even if you never sign out, a session token is only valid for 8 hours. After it expires you are asked to sign in again.

## Signing out and multiple companies

If you belong to several companies, your active session is scoped to the company you last selected or switched to. Signing out ends that session entirely. The next time you sign in, you choose a company again. To move between companies without ending your session, use the company switcher instead of signing out.

## Shared and public computers

> **Warning:** Always sign out when you finish on a shared, public, or borrowed device. Simply closing the browser tab may leave your session token in place until it expires, which could let someone else continue your session.

- Sign out explicitly before you leave the device.
- Avoid saving your password in a shared browser.
- If you suspect someone used your session, reset your password. Resetting also clears any account lockout and forces a fresh sign-in.

## Troubleshooting

| Situation | What to do |
| --- | --- |
| You are signed out unexpectedly | Your 8-hour session likely expired. Sign in again. |
| You are still signed in on another device | Sessions are per device. Sign out on each device, or wait for the token there to expire. |
| You want to end all access after a lost device | Reset your password. This is the fastest way to invalidate access that relies on your credentials. |

## Related

- Signing in with your email and password
- Switching between companies without signing out
- Resetting your password
- Account lockout: what it is and how to recover

# Signing in with ADFS or a generic OIDC provider

MobilityManager can use Active Directory Federation Services (ADFS) or any standards-compliant OpenID Connect (OIDC) provider for single sign-on. This article covers both, for users and for the administrator who configures them.

## Signing in

1. Open the sign-in page and select your company (tenant).
2. Choose the single sign-on button. The default label is **Login with ADFS** for ADFS, or **Login with SSO** for a generic OIDC provider. Your administrator may set a custom label.
3. You are redirected to your provider to authenticate, then returned to MobilityManager, which validates the response and starts your session.

> **Note:** On your first SSO sign-in, an account is created automatically with the Driver role. See "How single sign-on users are provisioned."

## Provider endpoints

MobilityManager builds the authorization request using the endpoint pattern for the selected provider:

| Provider | Authorization endpoint | Default button text |
| --- | --- | --- |
| `ADFS` | `{authority}/oauth2/authorize` | Login with ADFS |
| Generic OIDC | `{authority}/authorize` | Login with SSO |

In both cases the request uses the authorization-code flow with a state value for CSRF protection.

## Requirements for a generic OIDC provider

To work as a generic OIDC provider, your identity provider must support standard OpenID Connect discovery. MobilityManager reads the provider's `/.well-known/openid-configuration` document from the configured Authority to find the issuer and signing keys, then validates the returned identity token's signature, issuer, audience, and lifetime.

## What your administrator configures

A SystemAdmin enters these values per company:

| Setting | Notes |
| --- | --- |
| Provider | `ADFS` for ADFS, or the generic OIDC option for other providers |
| Authority | The base URL of the provider that serves the OIDC discovery document |
| Client ID | The application or relying-party identifier registered at the provider |
| Client secret | Stored securely and never displayed back in the app |
| Redirect URI | The MobilityManager callback URL, registered at the provider |
| Scopes | Defaults to `openid profile email` |
| Button text | Optional custom label for the sign-in button |

> **Tip:** For the returned token to validate, the provider must issue the email and a stable subject claim, and its audience must equal the configured client ID. Providers that expose a standard discovery document and JWKS endpoint work without provider-specific tuning.

## Troubleshooting

| Symptom | Likely cause |
| --- | --- |
| "Identity provider token validation failed" | Discovery document unreachable, wrong issuer or audience, or expired token. |
| "OIDC configuration is incomplete for this tenant" | Authority or client ID is missing. |
| Sign-in works but no email is recorded | The provider did not include an email claim in the identity token. |

## Related

- Single sign-on: an overview
- Signing in with Azure AD (Microsoft Entra ID)
- Signing in with Keycloak
- How single sign-on users are provisioned

# Signing in with Azure AD (Microsoft Entra ID)

This article explains how single sign-on with Azure Active Directory (Microsoft Entra ID) works in MobilityManager, both for people signing in and for the administrator who configures it.

## Signing in with Microsoft

1. Open the sign-in page and select your company (tenant).
2. Choose the single sign-on button. When the provider is Azure AD, its default label is **Login with Microsoft** (your administrator may have set a custom label).
3. You are redirected to Microsoft to sign in with your organizational account and complete any multi-factor prompts.
4. Microsoft returns you to MobilityManager, which verifies the response and starts your session.

> **Note:** If this is your first time signing in with Azure AD, an account is created for you automatically with the Driver role. See "How single sign-on users are provisioned."

## What your administrator configures

SSO is set up per company by a SystemAdmin. For Azure AD, register an application in your Microsoft Entra tenant and enter these values in MobilityManager:

| Setting | Value for Azure AD |
| --- | --- |
| Provider | `AzureAD` |
| Authority | Your Entra authority URL, for example `https://login.microsoftonline.com/<directory-id>/v2.0` |
| Client ID | The application (client) ID from the app registration |
| Client secret | A client secret created for the app registration (stored securely, never displayed back) |
| Redirect URI | The MobilityManager callback URL, also registered as a redirect URI in Azure |
| Scopes | Defaults to `openid profile email` |
| Button text | Optional; defaults to "Login with Microsoft" |

## How the Azure AD flow works

When a user starts SSO, MobilityManager builds an authorization request to the Azure endpoint at `{authority}/oauth2/v2.0/authorize`, requesting an authorization code. For Azure AD, the request also includes a nonce and a state value for added protection. After the user authenticates, Microsoft returns an identity token that MobilityManager validates against Azure's published signing keys, issuer, and audience before creating the session.

> **Tip:** Make sure the redirect URI registered in Azure exactly matches the redirect URI configured in MobilityManager, including scheme and path. A mismatch is the most common cause of a failed Azure AD sign-in.

## Troubleshooting

| Symptom | Likely cause |
| --- | --- |
| Redirect fails at Microsoft | Redirect URI not registered or misspelled in the app registration. |
| "Identity provider token validation failed" | Wrong authority, wrong client ID as audience, or a clock skew beyond the allowed tolerance. |
| "OIDC configuration is incomplete for this tenant" | Authority or client ID is missing in the company's SSO settings. |
| An email account already exists | A local password account uses the same email; an administrator must link it explicitly. |

## Related

- Single sign-on: an overview
- Signing in with Keycloak
- Signing in with ADFS or a generic OIDC provider
- How single sign-on users are provisioned

# Signing in with Keycloak

This article explains how single sign-on with Keycloak works in MobilityManager, for both users and the administrator who sets it up.

## Signing in with Keycloak

1. Open the sign-in page and select your company (tenant).
2. Choose the single sign-on button. When the provider is Keycloak, its default label is **Login with Keycloak** (your administrator may have set a custom label).
3. You are redirected to your Keycloak realm to sign in.
4. Keycloak returns you to MobilityManager, which verifies the response and starts your session.

> **Note:** On your first Keycloak sign-in, an account is created automatically with the Driver role. See "How single sign-on users are provisioned."

## What your administrator configures

A SystemAdmin creates a client in your Keycloak realm and enters these values per company in MobilityManager:

| Setting | Value for Keycloak |
| --- | --- |
| Provider | `Keycloak` |
| Authority | Your realm URL, for example `https://keycloak.example.com/realms/<realm>` |
| Client ID | The client ID configured in the Keycloak realm |
| Client secret | The client secret for a confidential client (stored securely, never displayed back) |
| Redirect URI | The MobilityManager callback URL, added as a valid redirect URI on the Keycloak client |
| Scopes | Defaults to `openid profile email` |
| Button text | Optional; defaults to "Login with Keycloak" |

## How the Keycloak flow works

When a user starts SSO, MobilityManager builds an authorization request to the Keycloak endpoint at `{authority}/protocol/openid-connect/auth`, requesting an authorization code, and includes a state value for CSRF protection. After the user authenticates, Keycloak returns an identity token that MobilityManager validates using the realm's published discovery document and signing keys, checking the signature, issuer, audience, and lifetime before starting the session.

> **Tip:** The Authority must point at the specific realm, not the Keycloak base URL. MobilityManager reads the realm's `/.well-known/openid-configuration` from that Authority to discover the endpoints and keys.

## Troubleshooting

| Symptom | Likely cause |
| --- | --- |
| "Invalid redirect\_uri" at Keycloak | The callback URL is not listed as a valid redirect URI on the client. |
| "Identity provider token validation failed" | Authority points at the wrong realm, the client ID does not match the token audience, or the client is public rather than confidential. |
| "OIDC configuration is incomplete for this tenant" | Authority or client ID is missing in the company's SSO settings. |
| An email account already exists | A local password account uses the same email; an administrator must link it explicitly. |

## Related

- Single sign-on: an overview
- Signing in with Azure AD (Microsoft Entra ID)
- Signing in with ADFS or a generic OIDC provider
- How single sign-on users are provisioned

# Single sign-on: an overview

Single sign-on (SSO) lets people sign in to MobilityManager with their existing organizational identity instead of a separate password. This article gives an overview of how SSO works, which providers are supported, and how it is set up.

## What SSO gives you

- Users sign in with the account they already use at work, through your identity provider.
- Passwords are managed centrally by your provider, not stored in MobilityManager.
- New users can be created automatically on their first successful SSO sign-in.

## Supported providers

MobilityManager supports OpenID Connect (OIDC) providers. Each company (tenant) is configured for one provider:

| Provider setting | Use it for | Default button text |
| --- | --- | --- |
| AzureAD | Microsoft Entra ID / Azure Active Directory | Login with Microsoft |
| Keycloak | Keycloak realms | Login with Keycloak |
| ADFS | Active Directory Federation Services | Login with ADFS |
| Generic OIDC | Any standards-compliant OIDC provider | Login with SSO |

> **Note:** Email and password sign-in always remains available alongside SSO. Enabling SSO for a company adds a single sign-on button; it does not disable local login.

## What signing in with SSO looks like

1. Select your company on the sign-in page. If SSO is enabled, a single sign-on button appears (with the provider's default text or a custom label set by your administrator).
2. Choose the SSO button. You are redirected to your identity provider to authenticate.
3. After you authenticate, your provider returns you to MobilityManager, which validates the response and starts your session.

## How the platform keeps it secure

When your provider returns a signed identity token, MobilityManager fetches the provider's published configuration and signing keys and verifies the token's signature, issuer, audience, and validity period before trusting it. Your identity is taken only from the verified token, never from unverified data. If any check fails, the sign-in is refused. A state value is also used during the redirect to guard against cross-site request forgery.

## Who configures SSO

SSO is configured per company by an administrator (SystemAdmin). The administrator enables SSO and supplies the connection details for your provider:

- **Provider** — AzureAD, Keycloak, ADFS, or generic OIDC.
- **Authority** — the base URL of your provider or realm.
- **Client ID** and **Client secret** — the application registered at your provider. The secret is stored securely and never shown back in the app.
- **Redirect URI** — where the provider returns users after they authenticate.
- **Scopes** — defaults to `openid profile email`.
- **Button text** — an optional custom label for the sign-in button.

> **Tip:** On the first SSO sign-in, new users are created automatically with the Driver role. See "How single sign-on users are provisioned" for details.

## Related

- Signing in with Azure AD (Microsoft Entra ID)
- Signing in with Keycloak
- Signing in with ADFS or a generic OIDC provider
- How single sign-on users are provisioned

# How single sign-on users are provisioned

When a person signs in through single sign-on (SSO) for the first time, MobilityManager can create their account automatically. This article explains how that provisioning works and what to expect afterward.

## Automatic account creation

If someone authenticates successfully at your identity provider but has no MobilityManager account yet, the platform creates one for them in the company (tenant) they signed in to. The new account is assigned the **Driver** role by default.

- A username is generated from the person's name, or from the part of their email before the @ sign.
- First and last names are taken from the verified identity token where available.
- No local password is set, because the account signs in through SSO.
- The account is active immediately and can sign in.

> **Note:** The default role for auto-provisioned SSO users is Driver. An administrator (SystemAdmin or FleetManager) can change the role afterward to grant more access.

## How existing accounts are matched

Before creating a new account, MobilityManager tries to match the sign-in to an existing user:

1. **By subject:** First it matches on the provider's stable subject identifier, which never changes for a given user. This is the most reliable link.
2. **By verified email:** If there is no subject match, it may match on email, but only when the provider marks the email as verified. An unverified email is never used to match an account.

> **Warning:** MobilityManager will not silently link an SSO identity to an existing local password account, even if the email matches. In that case sign-in is refused with a message asking you to contact your administrator, who must link the accounts explicitly. This prevents account takeover.

## Licensed user limit

Creating a brand-new account through SSO consumes a licensed user seat. If your company has reached its licensed user limit, auto-provisioning is blocked and the person sees a message to contact their administrator. Users who already exist can still sign in; only new account creation is affected.

## Driver profile

Because the default role is Driver, MobilityManager also ensures a driver profile exists for the new active user in that company, so they are ready to be assigned vehicles and complete driver tasks such as the Führerscheinkontrolle (driver's licence check).

## What administrators should do next

- Review newly provisioned users and adjust their role if Driver is not appropriate.
- Link any pre-existing local accounts to their SSO identity where users report a "contact your administrator" message.
- Monitor the licensed user count so SSO sign-ins are not blocked at the seat limit.

## Related

- Single sign-on: an overview
- Signing in with Azure AD (Microsoft Entra ID)
- Signing in with Keycloak
- Signing in with ADFS or a generic OIDC provider

# Switching between companies without signing out

If your account belongs to more than one company, you can move between them without signing out. This article explains how the company switch works and what changes when you use it.

## When you can switch

MobilityManager is multi-tenant, and a single account can hold roles in several companies (tenants). A company switcher becomes available once you have access to more than one company. If you belong to just one company, there is nothing to switch to.

## Switch companies step by step

1. While signed in, open the company switcher (usually near your account or company name).
2. Choose the company you want to work in.
3. Your session updates to the selected company. You do not need to re-enter your password.

Switching issues a fresh session for the target company. Your previous company context is replaced by the new one for the rest of your session.

## What changes after you switch

- **Roles and permissions:** Your session loads only the roles you hold in the company you switched to. If you are a FleetManager in one company and a Driver in another, your available actions change accordingly.
- **Data scope:** You now see the fleet, drivers, documents, and settings of the new company only.
- **Company context:** New records you create belong to the company you are currently in.

> **Note:** Switching companies does not sign you out or shorten your session. Your overall sign-in session still follows the normal 8-hour limit.

## Access rules

You can switch only to a company where you have a role. If you try to switch to a company you do not belong to, the switch is denied with "User does not have access to this tenant." The SystemAdmin role is the exception and may switch into any company; when a SystemAdmin enters a company they are not directly assigned to, they carry the SystemAdmin role there.

| Situation | Result |
| --- | --- |
| You have a role in the target company | Switch succeeds with that company's roles. |
| You have no role there and are not a SystemAdmin | Switch is denied. |
| The target company is deactivated | Switch is refused with a message to contact your administrator. |
| You are a SystemAdmin | You can switch into any active company. |

> **Tip:** If a company you expect is missing from the switcher, it may be deactivated, or you may not have a role there. Ask an administrator to check your access.

## Related

- Choosing your company at login
- Signing in with your email and password
- Signing out of MobilityManager
- Single sign-on: an overview

# The compliance trail and retention

Every action taken on a **Beleg** (receipt/expense document) is written to an immutable, append-only audit trail, and every automated data-retention run is logged. This article explains how to query the compliance trail and read retention status. Both require the `belege:compliance-trail` permission, which is granted to fleet-manager and administrator roles.

## What the compliance trail records

The trail is the governance backbone of the Belege module. It captures workflow events as they happen and cannot be edited after the fact. Recorded events include, among others:

- State transitions and denied transitions (including the reason a transition was refused).
- Submit denials that failed validation.
- Warning overrides, with the reason and the acting user.
- Financial corrections (before-and-after values) and any denied correction attempts.
- Financial snapshots captured at approval and payment.

Each entry stores the action, the entity type and id, the acting user and email, a timestamp, and a payload with the event details.

## Searching the access trail

You can query the trail with optional filters and page through the results:

- **Entity** — narrow to a specific Beleg.
- **Driver** — return events for Belege linked to a particular driver.
- **Start and end date** — bound the time window.

Results are returned in chronological order (oldest first) so you can read an entity's history as a timeline, together with a total count for the matching filter.

> **Tip:** To reconstruct the full life of one Beleg — who submitted, reviewed, overrode, corrected, approved, and paid it — filter by that Beleg's entity id and read the timeline top to bottom.

## Reading retention status

A scheduled retention process removes aged records according to your company's retention policy, and each run is logged immutably per company. The retention status view lists recent runs, most recent first, so you can confirm the process is executing and healthy. Each run entry shows:

| Field | Meaning |
| --- | --- |
| Run time | When the retention run executed. |
| Success | Whether the run completed without error. |
| Error message | The failure detail when a run did not succeed. |
| Records deleted | How many aged records and audit entries were removed. |

## Compliance reporting trends

Beyond the raw trail, a compliance reporting dashboard aggregates warning trends, override trends, and record counts by Beleg type over a date range. This reporting view is gated by the reports permission (`reports:view-all`) rather than the compliance-trail permission, so it may be available to a different set of users.

## Related

- Rule warnings, blockers, and override governance
- Making a financial correction during review
- Approving a Beleg and how approval locks the record
- The expense dashboard and exports

# The expense dashboard and exports

The expense dashboard aggregates your approved and paid **Belege** (receipt/expense documents) into six panels, and the export feature turns any dashboard or list view into a downloadable Excel or PDF file. This article covers both. Viewing and exporting require the `belege:view` permission.

## What the dashboard shows

Only **Approved** and **Paid** Belege contribute to the figures, so the dashboard reflects committed expenses rather than drafts under review. You can filter by date range, Beleg type, vehicle, driver, and cost center. Amounts are aggregated from line items.

### The six panels

| Panel | What it shows |
| --- | --- |
| Cost center breakdown | Total gross per cost center (Kostenstelle); line items without one are grouped as unassigned. |
| Vehicle summaries | Total gross per vehicle, each with a monthly trend. |
| Driver summaries | Total gross per driver, each with a monthly trend. |
| Tax summary | Total VAT (Umsatzsteuer) grouped by tax rate. |
| Trends | Total gross by month and by quarter. |
| Unassigned receipts | Belege missing a vehicle or driver link (count plus a sample list). |

An optional **damage costs** panel can be included; it summarizes damage-case financials (net, gross, Eigenanteil / deductible, and case count) alongside expense data. The response also reports how long the query took, so slow filters are visible.

## Creating an export job

An export captures the current view as a file. You choose two things:

- **Format** — Excel (a delimited, spreadsheet-ready file) or PDF.
- **Scope** — *dashboard* (the aggregated panels) or *list* (one row per line item across the matching Belege, with net, VAT, gross, warning and blocker counts, and reconcilable totals).

To create an export:

1. Set the filters you want the export to reflect (date range, Beleg type, vehicle, driver, cost center, status, and warning/blocker or unassigned flags).
2. Choose the format and scope and start the export.
3. The job is created, generated, and marked **completed** — or **failed** with an error message if generation could not finish.

> **Note:** The export language follows your request language. A German export uses a semicolon delimiter and comma decimals so it opens cleanly in a German-configured spreadsheet, and Beleg type and status labels are localized.

## Downloading and finding past exports

Once a job is **completed**, download its file from the job. The export history lists previous jobs and can be filtered by requesting user, date range, format, and status, so you can retrieve an earlier export without regenerating it. Each history entry shows who requested it, when, its format and scope, and a summary of the filters that were applied.

## Related

- The compliance trail and retention
- Reviewing Belege in the review queue
- Marking a Beleg as paid
- Rule warnings, blockers, and override governance

# Making a financial correction during review

A financial correction re-tags a **Beleg** (receipt/expense document) with a corrected VAT rate, cost center, or GL mapping and recomputes its amounts. This article explains how corrections work, when they are allowed, and why they must be made during review — not after approval. The action requires the `belege:correct-financial` permission.

## What a financial correction changes

A correction lets you set any combination of three financial tags on a Beleg:

- **VAT rate** (Umsatzsteuersatz) — the tax rate percentage.
- **Cost center** (Kostenstelle) — the internal charge code.
- **GL mapping** (Sachkonto) — the general-ledger account code.

The correction re-tags the *whole* Beleg: the corrected values are propagated to every line item, and for each line item the VAT amount and gross amount are recomputed from its net amount. The Beleg header totals are then recalculated so the document stays internally consistent.

## When a correction is allowed

Corrections are a review-stage activity. They are permitted while the Beleg is in an editable state (Draft, Submitted, In Review, or Needs Clarification) and are **blocked in immutable states** — Approved, Paid, and Rejected. Attempting to correct an immutable Beleg is refused with a "cannot be modified in terminal state" error and the denial is audited.

> **Warning:** There is no path back from Approved to In Review, and Approved is immutable. If you spot a financial mistake after approval you cannot correct it in place — so make corrections *before* you approve.

## Applying a correction

1. Open a Beleg that is still in an editable review state.
2. Enter the corrected VAT rate, cost center, and/or GL mapping.
3. Submit the correction. The platform validates the values, re-tags the line items, recomputes amounts, and saves.

## Validation against active reference data

Corrected values are checked against your company's *active* financial reference data. If a VAT rate, cost center, or GL code is not on the active list, the correction is rejected as "financial reference is not active". VAT rates are rounded to two decimal places, and cost center and GL codes are matched case-insensitively. Keep your financial reference data current so valid corrections are accepted.

## How corrections are audited

Every correction is recorded with a full before-and-after picture — the previous and new VAT rate, cost center, GL mapping, net total, and gross total. The data change and its audit record are committed together in a single transaction, so a financial change can never be saved without its audit entry. Denied attempts (unauthorized user or immutable state) are also recorded.

## Related

- Approving a Beleg and how approval locks the record
- Reviewing Belege in the review queue
- Rule warnings, blockers, and override governance
- The compliance trail and retention

# Marking a Beleg as paid

Marking a **Beleg** (receipt/expense document) as paid is the final step of the workflow: it confirms that an approved expense has been settled and closes the record. This article explains who can do it, the segregation-of-duties check, and how the small-tenant mode differs. The action requires the `belege:mark-paid` permission.

## When a Beleg can be marked as paid

Only an **Approved** Beleg can move to **Paid**. Paid is a terminal, immutable state — once set, there are no further transitions and the record cannot be edited. If a Beleg is not yet approved, mark-paid is not available; complete review and approval first.

## Marking a Beleg as paid

1. Open an Approved Beleg, or select one or more Approved Belege in the review queue.
2. Choose **Mark paid**. You may add an optional payment-confirmation note.
3. The Beleg moves to Paid, the note is persisted to the audit trail, and the record locks.

> **Tip:** Use the optional note to record a payment reference (for example a bank run or batch identifier). It is stored with the transition so the payment can be reconciled later.

## Segregation of duties on payment

By default you cannot confirm payment on a Beleg you approved — the approver and the person confirming payment must be different people. Attempting to do both is refused as a maker-checker violation and recorded. A company setting can allow self-approval; when enabled, the same person may approve and mark paid, but the action is flagged as a self-approval in the audit trail.

## Snapshots are re-confirmed at payment

Marking as paid re-runs the financial snapshot step. Because snapshots are only written when they are not already present, the values captured at approval are preserved unchanged. A *financial snapshots taken* event is written alongside the payment transition, giving auditors a complete record at both approval and payment.

## Small-tenant (no-approval) mode

Companies configured in the no-approval mode skip the review, approval, and separate payment steps entirely: submitting a Beleg records it **directly as Paid**. In this mode:

- Reaching Paid is a property of the configured workflow, so no separate `belege:mark-paid` permission is needed — the submit permission (`belege:transition`) is sufficient.
- Data-quality **Blocking** rules are still enforced at submit, so a Beleg with a hard-stop problem cannot be recorded as paid.
- Warnings do not block (there is no human approver to override them), and maker-checker does not apply because the flow is single-person.

## Permissions summary

| Mode | Permission to reach Paid |
| --- | --- |
| Review (full flow) | `belege:mark-paid` |
| None (no-approval) | `belege:transition` (via submit) |

## Related

- Approving a Beleg and how approval locks the record
- Understanding the Beleg approval workflow and states
- Making a financial correction during review
- The compliance trail and retention

# Attach a receipt document to a Beleg

This article shows you how to attach the scanned receipt or invoice document to a Beleg (receipt/expense document) so reviewers have the source evidence. Uploading documents requires the **documents:manage** permission; viewing them requires **documents:view**. Both are held by FleetManager and SystemAdmin.

## How Beleg documents are stored

Beleg attachments go through the shared DocumentModule, not a separate store. That means every attachment is encrypted, tenant-scoped, and versioned. When you upload a file, the platform records a document with its metadata (file name, content type, size, category, and uploader) and stores the file content as version 1. Uploading a replacement later adds a new version rather than overwriting the old one, and downloads always return the latest version.

## Attach a document

1. Open the target Beleg. The Beleg must already exist; uploading against a missing Beleg returns `BELEG_NOT_FOUND`.
2. In the Documents section, choose the file to upload (for example a PDF or photo of the receipt).
3. Select a document **category**. A category is required and must match an active system document category.
4. Upload. The file is sent to the DocumentModule with the association type *Beleg* and the Beleg's ID, then stored and version-tracked.

> **Note:** The category is mandatory. If you leave it blank or pick a value that is not an active system document category, the upload is rejected with the message "Please select a document category" (or a category-validation error). Category values are normalized to lowercase before matching.

## View and download attachments

Anyone with **documents:view** can list a Beleg's attachments and open them. The metadata list shows the file name, content type, size, category, and upload date, newest first. Downloading streams the latest stored version back with its original file name and content type. If the document, the Beleg link, or the stored file cannot be found, the platform returns a generic `DOCUMENT_ACCESS_DENIED` ("You do not have permission to view this document") rather than revealing whether the file exists.

## What gets recorded

- The uploader's user ID and email are stored on the document for the audit trail.
- The first upload creates version 1; each subsequent upload increments the version number.
- Attachments stay linked to the Beleg through its whole lifecycle, including after it is Approved or Paid.

> **Tip:** Attach the receipt before you submit the Beleg. Reviewers rely on the document to confirm the vendor, amounts, and VAT shown on the line items, and a missing receipt is a common reason a Beleg is returned to Needs Clarification.

## Attachments and export artifacts are different

Receipt attachments you add here are ordinary Beleg documents. They are separate from export artifacts (the Excel or PDF files produced by the expense export jobs), which the DocumentModule stores under the `beleg-export` association type and hides from standard document lists. You will not see export files in the Beleg's attachment list.

## Related

- Capture a new Beleg and its header fields
- What Belege (receipt/expense) management does
- When you can edit a Beleg: editable states
- Add line items to a Beleg

# Capture a new Beleg and its header fields

This article shows you how to capture a new Beleg (receipt/expense document) as a draft and explains every header field you can set. Creating and editing Belege requires the **belege:transition** permission, held by FleetManager and SystemAdmin.

## Before you start

A Beleg can only be created when your tenant has an active Belege configuration baseline. If none is published, the create action returns `CONFIGURATION_BASELINE_NOT_AVAILABLE` and no Beleg is saved. Ask a SystemAdmin to activate a configuration version first.

## Create the draft

1. Open the Belege module and start a new Beleg (intake form).
2. Fill in the header fields described below.
3. Optionally add line items now, or save the draft and add them later.
4. Save. The Beleg is created in **Draft** status and recorded against the current configuration version for traceability.

> **Note:** On creation, line items are optional. You can save a header-only draft and return to it. When you later update the draft, at least one line item becomes required.

## Header fields

| Field | Required | Notes |
| --- | --- | --- |
| Beleg type | Yes | The kind of expense, for example fuel, toll, or maintenance. Up to 50 characters. |
| Beleg date | No | Used for filtering and reporting. Defaults to today (UTC) if left blank; the time part is dropped. |
| Currency | No | A 3-letter ISO 4217 code. If omitted, it defaults to your tenant currency. If provided, it must be valid and must match the tenant currency. |
| Vendor | No | The supplier name shown on the receipt. Up to 200 characters. |
| Invoice number | No | The vendor's invoice reference. Up to 100 characters. |
| Invoice mileage | No | Odometer reading on the receipt; used for plausibility checks by the rules engine. |
| Vehicle | No | The mapped vehicle. Optional at capture, but required to submit. |
| Driver | No | The mapped driver. Optional at capture, but required to submit. |

> **Warning:** A tenant keeps a single currency. Entering a currency that differs from the configured tenant currency is rejected with a validation error, because cross-Beleg dashboard and export totals are only meaningful in one currency.

## Vehicle and driver mapping

Vehicle and driver are optional while the Beleg is a draft, which lets you capture receipts before you know which vehicle they belong to. Unmapped drafts appear in the dashboard's "unassigned receipts" panel. To submit the Beleg into review, however, it must have exactly one vehicle and one driver assigned; submitting without them returns a validation error pointing at the missing mapping.

## After saving

Your new Beleg lands in Draft with computed totals of zero (until line items are added). From here you can:

- Add or edit line items and financial tags.
- Attach the scanned receipt document.
- Submit the Beleg once a vehicle and driver are mapped and at least one line item exists.

## Related

- What Belege (receipt/expense) management does
- Add line items to a Beleg
- When you can edit a Beleg: editable states
- Attach a receipt document to a Beleg

# What Belege (receipt/expense) management does

Belege management is the module in MobilityManager where you capture, tag, review, and pay off fleet receipts and invoices. This article explains what a Beleg (receipt/expense document) is, the lifecycle it moves through, and who can work with it.

## What a Beleg is

A *Beleg* is a single tenant-scoped expense record — a fuel receipt, a toll charge, a maintenance invoice, and so on. Each Beleg has a header (type, date, vendor, currency, optional vehicle and driver mapping) and one or more Positionen (line items) that carry the amounts and financial tags. The header totals are always derived from the line items, so the line items are the authoritative source of the money on a Beleg.

## What the module lets you do

- **Capture** a new Beleg as a draft, with header fields and up to 20 line items.
- **Tag** each line item with a VAT rate (USt), cost center (Kostenstelle), and GL mapping (general-ledger account), validated against your active financial reference data.
- **Attach** the scanned receipt or invoice document to the Beleg for evidence.
- **Submit** the Beleg into a review workflow, where finance can add clarifications, apply financial corrections, and approve it.
- **Pay** an approved Beleg and see it feed the six-panel expense dashboard and exports.

## The Beleg lifecycle

Every Beleg carries a current status plus an immutable history of transitions. The states are:

| State | Meaning |
| --- | --- |
| Draft | Being captured; fully editable. |
| Submitted | Handed to finance; awaiting pickup. |
| In Review | Finance is reviewing rule outcomes and financials. |
| Needs Clarification | Returned for corrections; editable again. |
| Approved | Accepted; financial snapshots taken; read-only. |
| Paid | Terminal; payment recorded. |
| Rejected | Terminal; not accepted. |

The forward path is Draft → Submitted → In Review → Approved → Paid, with Needs Clarification and Rejected as side paths. Approved, Paid, and Rejected are immutable: their header, financial, and line-item data can no longer be edited.

> **Note:** Only Draft and Needs Clarification Belege can be edited. Once a Beleg is Approved, its cost center, VAT rate, and GL mapping are snapshotted from the current reference data and frozen for audit.

## Who can use it

Belege management is a finance and admin area, gated by permissions rather than by role name:

- **belege:view** — see the list, detail, review queue, and dashboard. Held by FleetManager and SystemAdmin.
- **belege:transition** — create, edit, submit, and move Belege through the workflow. Held by FleetManager and SystemAdmin.
- **belege:mark-paid** — move an Approved Beleg to Paid. Held by FleetManager and SystemAdmin.
- **belege:correct-financial** — retag financials during review. SystemAdmin only, as it is the most financially sensitive action.

The Driver role deliberately has no Belege permissions, so the Expenses navigation and expense data stay within finance and administration.

## Related

- Capture a new Beleg and its header fields
- Add line items to a Beleg
- When you can edit a Beleg: editable states
- Attach a receipt document to a Beleg

# When you can edit a Beleg: editable states

A Beleg (receipt/expense document) can only be edited in certain workflow states. This article explains which states are editable, which are locked, and what happens when you try to edit a Beleg that has moved on.

## The two editable states

You can change a Beleg's header, financial tags, and line items only while it is in one of these states:

- **Draft** — the Beleg is still being captured and has not been submitted.
- **Needs Clarification** — finance returned the Beleg for corrections during review.

In both states the update flow behaves the same way: it re-validates the line items, re-pins the Beleg to the currently active configuration version, recomputes all amounts and header totals, re-evaluates the applicable rules, and records audit events for the change.

> **Tip:** Because editing a Needs Clarification Beleg re-pins it to the active configuration version, rule changes published after the Beleg was first created (for example disabling a cost-center rule) take effect the next time you save your edits.

## States you cannot edit

| State | Editable? | What you get if you try |
| --- | --- | --- |
| Draft | Yes | Edits are saved. |
| Needs Clarification | Yes | Edits are saved. |
| Submitted | No | `BELEG_NOT_DRAFT` — only draft or clarification Belege can be updated. |
| In Review | No | `BELEG_NOT_DRAFT` — the Beleg is with finance. |
| Approved | No | `BELEG_IMMUTABLE` — header, financial, and line-item edits are rejected. |
| Paid | No | `BELEG_IMMUTABLE` — terminal state. |
| Rejected | No | `BELEG_IMMUTABLE` — terminal state. |

## Why Submitted and In Review are locked

Once you submit a Beleg it is handed to finance for review, so the capture form stops accepting changes to keep the record stable while it is being assessed. If you need to change something at this stage, finance can send the Beleg back to **Needs Clarification**, which makes it editable again, or an authorized reviewer can apply a financial correction in place.

## Why Approved, Paid, and Rejected are immutable

Approved, Paid, and Rejected are the module's immutable states. When a Beleg is approved, each line item's cost center, VAT rate, and GL mapping are snapshotted from the current reference data and frozen. Locking these states protects the audit trail: the financial data that was approved and paid can never be altered after the fact. Approved Belege remain transitionable to Paid but are otherwise read-only.

> **Note:** Editing and workflow are separately permissioned. Making edits needs **belege:transition**; retagging financials during review needs **belege:correct-financial** (SystemAdmin only).

## How to get an editable Beleg back

1. If the Beleg is Submitted or In Review, ask a reviewer to move it to Needs Clarification.
2. Once it is in Needs Clarification, open it and make your changes.
3. Save, then resubmit or return it to review.

## Related

- Capture a new Beleg and its header fields
- Edit line items: create, update, and delete
- What Belege (receipt/expense) management does
- How header totals are recomputed on every change

# Add line items to a Beleg

Line items (Positionen) carry the amounts on a Beleg (receipt/expense document). This article shows you how to add them, what each field means, and the limits that apply. Adding line items requires the **belege:transition** permission and a Beleg in an editable state (Draft or Needs Clarification).

## Add a line item

1. Open the Beleg in Draft or Needs Clarification.
2. In the Positionen grid, add a row.
3. Enter the description, quantity, and unit price. Optionally set the VAT rate, cost center, and GL mapping, and the position type.
4. Save the Beleg. All line items are sent together and the amounts and header totals are recomputed on save.

> **Note:** Line items are submitted as a single array with the Beleg, not one at a time. Saving applies the whole set at once and recomputes every amount.

## Line item fields

| Field | Required | Notes |
| --- | --- | --- |
| Description | Yes | What the line covers, for example "Diesel" or "AdBlue". Up to 500 characters; leading/trailing spaces are trimmed. |
| Quantity | Yes | Must be greater than zero. Stored to 4 decimal places. |
| Unit price | Yes | Must be zero or greater. Stored to 2 decimal places. |
| VAT rate (USt) | No | Percentage, for example 19. Validated against your active VAT rate options. |
| Cost center (Kostenstelle) | No | Validated against your active cost center options (case-insensitive). |
| GL mapping | No | General-ledger account code, validated against your active GL mapping options. |
| Position type | No | Category of the line, for example fuel, washing, toll, maintenance\_repair, or other. |
| Fuel type | No | References the fuel type catalog; relevant when the position type is fuel. |
| Sort order | No | Controls row order. If omitted, rows are numbered in the order you enter them. |

## The 20 line item limit

A Beleg can hold at most **20** line items. Sending more is rejected with `BELEG_LINE_ITEM_LIMIT_EXCEEDED` ("Maximum of 20 line items reached") and nothing is saved. In the UI the Add action is disabled once you reach 20 rows.

## Validation you should expect

- A blank description is rejected: "Line item {n} description is required".
- A quantity of zero or less is rejected: "Quantity must be greater than zero".
- A negative unit price is rejected: unit price must be zero or greater.
- A VAT rate, cost center, or GL mapping that is not an active reference option is rejected with `FINANCIAL_REFERENCE_NOT_ACTIVE`.

When you update a Beleg (rather than creating one), at least one line item is required — an update with zero line items is rejected.

> **Tip:** Rows are automatically renumbered 1…n by sort order on save, so you do not need to keep the numbering gap-free yourself. Reorder rows and the platform re-sequences them for you.

## What happens on save

For each line item the platform computes the net, VAT, and gross amounts, then sums them into the Beleg header totals. It re-evaluates the applicable rules and records audit events for the line-item change and the totals recomputation, all within a single transaction.

## Related

- Tag line items with VAT rate, cost center, and GL mapping
- How net, VAT, and gross amounts are calculated
- Edit line items: create, update, and delete
- How header totals are recomputed on every change

# Edit line items: create, update, and delete

When you edit a Beleg (receipt/expense document), you send the full set of line items you want to keep, and the platform reconciles them against what is already stored. This article explains that full-replacement model — how rows are created, updated, and deleted in one save. Editing requires the **belege:transition** permission and a Beleg in Draft or Needs Clarification.

## Full-replacement, not incremental

There is no "delete row 3" or "add one row" call. Instead, every update carries the complete line-item array the Beleg should have after the save. The platform compares that array with the stored line items and works out what to create, update, and delete. This all-or-nothing approach is what keeps the header totals consistent — line items are never mutated outside this flow.

## How each row is matched

Matching is done by the line item's `id`:

| Row in your request | Meaning | Result |
| --- | --- | --- |
| No id (null) | New row | Created as a new line item. |
| Has an id that exists on the Beleg | Existing row | Updated in place with your values. |
| An existing id you omit from the request | Removed row | Deleted from the Beleg. |

> **Warning:** Anything you leave out is deleted. To keep a line item, include it in the array with its existing id. To keep it unchanged, resend it with the same values.

## A worked example

Suppose a Beleg has line items with ids 10, 11, and 12. You save an update containing:

- id 10 with a corrected quantity — item 10 is **updated**.
- id 12 unchanged — item 12 is **kept**.
- a row with no id — a **new** item is created.
- id 11 is not present — item 11 is **deleted**.

The result is three line items: the edited 10, the untouched 12, and the new one.

## What happens on save

1. The line items are validated at the boundary: required fields per line, at least one line item, no more than 20, and active financial references.
2. The Beleg is loaded with its existing line items and confirmed to be in an editable state.
3. Full replacement is applied: rows are created, updated, or deleted by id match.
4. Every line item's net, VAT, and gross amount is recomputed and rows are re-sequenced 1…n by sort order.
5. The header totals are recomputed.
6. Everything is persisted in a single transaction, the applicable rules are re-evaluated, and audit events (`BelegLineItemsUpdated`, `BelegTotalsRecomputed`) are recorded.

> **Note:** The whole save is atomic. If any line fails validation — for example an inactive cost center or a 21st row — nothing is written and the Beleg keeps its previous line items and totals.

## Reordering rows

You can set a sort order on each row to control its position. After the save, the platform normalizes the sort order to a gap-free 1…n sequence based on the values you sent (ties broken by id), so drag-and-drop reordering in the grid persists cleanly without you managing the numbers.

## Related

- Add line items to a Beleg
- How header totals are recomputed on every change
- How net, VAT, and gross amounts are calculated
- When you can edit a Beleg: editable states

# Financial reference data: cost centers, VAT rates, and GL codes

Financial reference data is the set of allowed cost centers (Kostenstellen), VAT rates (USt), and GL mapping codes that Beleg line items can be tagged with. This article explains what the reference data contains, how it is maintained, and how it gates line-item tagging.

## The three reference lists

| List | Key fields | Used for |
| --- | --- | --- |
| VAT rates | Percentage, display name, sort order, active flag | The VAT rate on a line item; drives the calculated VAT amount. |
| Cost centers | Code, display name, sort order, active flag | The cost center tag on a line item; drives cost-center reporting. |
| GL mappings | Code, display name, sort order, active flag | The general-ledger account a line item books to. |

All three are tenant-scoped: each tenant maintains its own lists. VAT rates are stored as percent values (for example 19.00). Cost center and GL codes are short codes (up to 50 characters) with a friendlier display name.

## Where it lives and who maintains it

Reference data is maintained through the canonical financial reference data capability, shared across the platform (it is also used by Strafmandate cost allocation and damage management). Access is permission-gated:

- **settings:view** — read the reference lists. Held by FleetManager and SystemAdmin.
- **settings:manage** — add, edit, activate, or deactivate options. Held by SystemAdmin (and any custom role granted it).

> **Note:** FleetManager can see the reference data but cannot change it. Changing which cost centers, VAT rates, or GL codes are available is a SystemAdmin task.

## Active vs. inactive options

Each option has an *active* flag. Only active options are offered when tagging line items, and only active options pass validation. Deactivating an option keeps it in the list for history but stops it being used on new or edited Belege.

Validation is enforced per list only when that list has at least one active option. If a tenant has no active cost centers configured, cost-center values are not checked at all — so configure your reference data before you expect tags to be enforced. Cost center and GL codes are matched case-insensitively; VAT rates are matched on the percentage rounded to two places.

## How reference data gates tagging

When you save a Beleg line item with a VAT rate, cost center, or GL mapping, each value is checked against the active options. A value that is not active is rejected with `FINANCIAL_REFERENCE_NOT_ACTIVE` and the whole save is refused. This ensures the finance system only ever receives codes it recognizes.

## Snapshots preserve what was approved

Because reference data can change over time, the platform snapshots each line item's cost center, VAT rate, and GL mapping when the Beleg is Approved. Those snapshot values are frozen, so a Beleg approved last quarter still shows exactly the codes and rate that applied then, even if you have since renamed or deactivated the option.

> **Tip:** Prefer deactivating an obsolete cost center over deleting it. Deactivation removes it from new tagging while keeping historical Belege and their snapshots readable. Note that one cost center can also be flagged as the default department allocation for Strafmandate (penalty notice) payments — at most one per tenant.

## Related

- Tag line items with VAT rate, cost center, and GL mapping
- Add line items to a Beleg
- How net, VAT, and gross amounts are calculated
- How header totals are recomputed on every change

# How header totals are recomputed on every change

The totals shown on a Beleg (receipt/expense document) header are always derived from its line items, never typed in. This article explains what the header totals are, when they are recalculated, and why they always match the sum of the Positionen (line items).

## The two header totals

A Beleg header carries two computed totals:

- **Total net amount** — the sum of every line item's net amount.
- **Total gross amount** — the sum of every line item's gross amount.

There is no separately stored header VAT total: the total VAT is simply total gross minus total net. The dashboard's tax summary reconstructs VAT by grouping the line VAT amounts per rate.

## Recomputed on every change

The header totals are recalculated through a single centralized recompute step every time the line items change. That step runs whenever:

- A Beleg draft is created with line items.
- A Beleg in Draft or Needs Clarification is updated.
- An authorized reviewer applies a financial correction during review.

Because recompute is the only place header totals are set, they can never drift out of sync with the line items. If you add, edit, remove, or reorder rows, the totals are re-summed from the current line items on save.

> **Note:** A header-only draft with no line items has totals of 0.00. As soon as you add priced line items and save, the totals reflect their sum.

## How a save updates the totals

1. Your line items are validated (required fields, the 20-item limit, active financial references).
2. Each line item's net, VAT, and gross amounts are computed and rounded to 2 places.
3. The line items are re-sequenced 1…n by sort order.
4. The header total net and total gross are re-summed from those line items.
5. The change is persisted in one transaction, rules are re-evaluated, and audit events are recorded.

## The audit trail for totals

Every line-item update records two immutable audit events: `BelegLineItemsUpdated` (with the Beleg ID, status, and line-item count) and `BelegTotalsRecomputed` (with the new total net and total gross). This gives finance a traceable history of how the totals evolved across edits.

> **Tip:** If a header total does not look right, check the individual line amounts rather than the header — the header is only ever a sum of the lines. A rounding or VAT-rate difference on one line is the usual cause.

## Totals after approval

Once a Beleg is Approved, its line items and therefore its totals are frozen. Approved, Paid, and Rejected Belege are immutable, so no further recompute occurs. Only approved and paid Belege contribute to the financial figures on the expense dashboard, so the frozen totals are what downstream reporting relies on.

## Related

- How net, VAT, and gross amounts are calculated
- Add line items to a Beleg
- Edit line items: create, update, and delete
- When you can edit a Beleg: editable states

# How net, VAT, and gross amounts are calculated

Every line item on a Beleg (receipt/expense document) has three calculated amounts: net, VAT, and gross. This article explains exactly how they are computed and rounded, so the numbers you see always reconcile. You do not enter these amounts; the platform derives them from quantity, unit price, and VAT rate.

## The formulas

For each line item, the amounts are computed in this order:

1. **Net amount** = quantity × unit price
2. **VAT amount** = net amount × (VAT rate ÷ 100)
3. **Gross amount** = net amount + VAT amount

If a line item has no VAT rate, the VAT amount is treated as zero and gross equals net.

## How values are rounded

Rounding is applied at each step using "round half away from zero" (MidpointRounding.AwayFromZero), so a half unit always rounds up in magnitude — 0.125 rounds to 0.13, not 0.12. The decimal places are:

| Value | Decimal places |
| --- | --- |
| Quantity | 4 |
| Unit price | 2 |
| VAT rate (percentage) | 2 |
| Net amount | 2 |
| VAT amount | 2 |
| Gross amount | 2 |

The inputs are rounded first (quantity to 4 places, unit price and VAT rate to 2), then net, VAT, and gross are each rounded to 2 places as they are computed.

## A worked example

Take a fuel line of 50 litres at 1.859 per litre with 19% VAT:

- Unit price rounds to **1.86** (2 places, away from zero).
- Net = 50 × 1.86 = **93.00**.
- VAT = 93.00 × 0.19 = 17.67 → **17.67**.
- Gross = 93.00 + 17.67 = **110.67**.

> **Note:** Because unit price is rounded to 2 places before the net is calculated, enter the price already rounded to your currency's minor unit if you want to avoid surprises. A raw price of 1.859 becomes 1.86 in the stored net.

## Why rounding at each step matters

Rounding the VAT amount per line (rather than only on the header total) means the sum of the line VAT amounts always equals the difference between the header gross and net totals. This keeps the Beleg internally consistent and makes the dashboard tax summary, which groups VAT by rate, reconcile against the line items.

## When amounts are recomputed

The amounts are recalculated every time the line items are saved — on create, on edit, and when an authorized reviewer applies a financial correction. You never edit net, VAT, or gross directly; changing the quantity, unit price, or VAT rate and saving is what updates them.

> **Tip:** A quantity must be greater than zero and a unit price must be zero or greater, so a valid line always produces a non-negative net. A zero-priced line (net 0.00) is allowed, for example a free item recorded for completeness.

## Related

- How header totals are recomputed on every change
- Add line items to a Beleg
- Tag line items with VAT rate, cost center, and GL mapping
- Edit line items: create, update, and delete

# Tag line items with VAT rate, cost center, and GL mapping

Financial tags tell your finance system how each line of a Beleg (receipt/expense document) should be taxed and booked. This article explains the three tags — VAT rate (USt), cost center (Kostenstelle), and GL mapping — how they are validated, and how they are frozen at approval. Tagging is done while editing a Beleg and requires the **belege:transition** permission.

## The three financial tags

| Tag | What it drives | Validated against |
| --- | --- | --- |
| VAT rate (USt) | The VAT amount and the tax summary on the dashboard. | Active VAT rate options (matched on the percentage, rounded to 2 places). |
| Cost center (Kostenstelle) | Cost-center breakdowns and filtering. | Active cost center options (matched case-insensitively). |
| GL mapping | The general-ledger account the expense books to. | Active GL mapping options (matched case-insensitively). |

All three tags are optional per line item, but if you set one it must correspond to an *active* reference option for your tenant.

## How validation works

When you save, each tag you supplied is checked against the active financial reference data:

- A VAT rate that is not in the active VAT options is rejected.
- A cost center code that is not an active cost center is rejected.
- A GL mapping code that is not an active GL mapping is rejected.

Any of these failures returns `FINANCIAL_REFERENCE_NOT_ACTIVE` ("Selected financial reference is not active") and the whole save is rejected — the Beleg is not partially updated.

> **Note:** Validation only fires when there is at least one active option of that kind. If your tenant has no active cost centers configured at all, cost-center values are not checked. Configure your reference data first so tags are enforced.

## Only the VAT rate changes the amounts

Of the three tags, only the VAT rate affects money. The net amount comes from quantity × unit price; the VAT amount is net × (VAT rate ÷ 100); gross is net + VAT. Cost center and GL mapping are classification tags — they route and report the expense but never change the calculated totals.

## Tags are snapshotted at approval

While a Beleg is editable, the tags are live references — change a code and it re-validates against current reference data. When the Beleg transitions to **Approved**, each line item's cost center, VAT rate, and GL mapping are copied into snapshot fields (CostCenterSnapshot, VatRateSnapshot, GlMappingSnapshot) taken from the reference data at that moment. Those snapshots are immutable and preserve exactly what was approved, even if you later rename or deactivate a reference option.

> **Warning:** Get the tags right before approval. After approval the Beleg is immutable, and the only way an authorized reviewer can retag it earlier is a financial correction (permission **belege:correct-financial**, SystemAdmin only) while it is still in a review state.

## Position type and fuel type

Alongside the financial tags, each line can carry a position type (for example fuel, washing, toll, maintenance\_repair, or other) and, for fuel lines, a fuel type key. These describe what the line is rather than how it is booked, and they help rules and reporting classify the expense.

## Related

- Add line items to a Beleg
- Financial reference data: cost centers, VAT rates, and GL codes
- How net, VAT, and gross amounts are calculated
- Edit line items: create, update, and delete

# Understanding the Beleg approval workflow and states

This article explains how a **Beleg** (receipt/expense document) travels through MobilityManager, the states it can hold, and the rules that decide which actions are available at each step. Knowing the state graph lets you predict what you can and cannot do to any given Beleg.

## The seven workflow states

A Beleg always holds exactly one state. The states model a governed review-and-payment lifecycle.

| State | Meaning |
| --- | --- |
| Draft | Created and still fully editable; not yet submitted. |
| Submitted | Sent for review; header and line items have passed submit validation. |
| In Review | A reviewer has taken it up; quality rules have been evaluated. |
| Needs Clarification | Returned to the submitter with a mandatory note. |
| Approved | Signed off; financially locked and awaiting payment. |
| Rejected | Declined; a terminal state. |
| Paid | Payment confirmed; a terminal state. |

## How a Beleg moves between states

Only the following transitions are permitted. Any request that is not on this list is refused and written to the audit trail.

- Draft → Submitted
- Submitted → In Review, Needs Clarification, or Rejected
- In Review → Approved, Needs Clarification, or Rejected
- Needs Clarification → Submitted or In Review
- Approved → Paid

Returning a Beleg to **Needs Clarification** requires a clarification note; the transition is rejected without one.

## Terminal and immutable states

Two related concepts govern what happens at the end of the lifecycle:

- **Terminal states** — Paid and Rejected. No further transitions are possible.
- **Immutable states** — Approved, Paid, and Rejected. These reject header, financial, and line-item edits. Approved can still move to Paid, but is otherwise read-only.

> **Note:** Because Approved is immutable, correct any financial data *before* you approve. Once approved, the record is locked and the only remaining step is Paid.

## Two approval modes per company

Each tenant (company) runs one of two workflow modes:

- **Review** — the full governed flow (Draft → Submitted → In Review → Approved → Paid) with segregation-of-duties (maker-checker) and the configurable rule gates.
- **None** — a small-tenant mode with no approval flow: submitting a Beleg records it directly as Paid. Data-quality Blocking rules are still enforced at submit, but warnings do not block (there is no human approver to override them).

If the company cannot be resolved, the platform fails closed to Review (the most restrictive mode). Self-approval is separately controlled by a company setting.

## What submit validation checks

To leave Draft, a Beleg must have a Vendor (Lieferant) and a Currency, and at least one line item whose description is filled in, quantity is greater than zero, and unit price is not negative. Financial tagging requirements (cost center, tax rate) are enforced later by the rules engine, not at submit.

## Permissions

The `belege:transition` permission is required to submit and to move a Beleg through review. The separate `belege:mark-paid` permission is required to move Approved → Paid. Every successful transition and every denial is recorded in the immutable audit trail.

## Related

- Reviewing Belege in the review queue
- Approving a Beleg and how approval locks the record
- Rule warnings, blockers, and override governance
- Marking a Beleg as paid

# Approving a Beleg and how approval locks the record

Approving a **Beleg** (receipt/expense document) is the sign-off step that clears it for payment. This article explains the checks the platform runs at the approval gate, the segregation-of-duties rule, and how approval captures immutable financial snapshots and locks the record. Approving requires the `belege:transition` permission.

## Where approval sits in the workflow

A Beleg is approved by moving it from **In Review** to **Approved**. Approval is only reachable from In Review — you cannot approve a Draft or a Submitted Beleg directly. Once approved, the only remaining transition is to **Paid**.

## Checks run at the approval gate

When you approve, the rules engine re-evaluates the Beleg and enforces two gates:

- **Blocking outcomes must be clear.** If any Blocking rule outcome is unresolved and not overridden, approval is refused with a "blocked by rule" error. Blockers cannot be overridden — you must fix the underlying data.
- **Warnings must be overridden.** If any Warning outcome is still open, approval is refused until each warning is overridden with a reason. Warnings are the "review and justify" tier; blockers are the "must fix" tier.

> **Tip:** Clear blockers while the Beleg is still In Review, then override remaining warnings, so the approval action itself succeeds on the first attempt.

## Segregation of duties (maker-checker)

By default you cannot approve a Beleg you submitted or created — the person who approves must be different from the person who prepared it. Attempting to self-approve is refused as a maker-checker violation and recorded in the audit trail. A company setting can permit self-approval; when it is enabled, the self-approval is allowed but flagged as such in the audit event so it remains visible to auditors.

## Financial snapshots captured on approval

Approval is the moment the platform freezes the financial classification of each line item. For every line item it records:

| Snapshot | What it captures |
| --- | --- |
| Cost center snapshot | The active cost center's display name (or its code) at approval time. |
| Tax rate snapshot | The line item's VAT rate (Umsatzsteuersatz) percentage. |
| GL mapping snapshot | The active GL account (Sachkonto) display name (or its code). |

Each snapshot is written only if it is not already set, so the values captured at approval are preserved even if the underlying reference data (cost centers, GL codes) is later renamed or deactivated. A *financial snapshots taken* event is written to the audit trail alongside the approval, and the approving user is stored on the Beleg.

## Why the record locks after approval

Approved is an **immutable** state: it rejects header, financial, and line-item edits. This protects the approved figures and their captured snapshots from silent change. If you discover a mistake after approval, you cannot edit the Beleg — corrections must be made earlier in the review stage. The same snapshot and lock behavior applies again when the Beleg is later marked as Paid.

## Related

- Understanding the Beleg approval workflow and states
- Rule warnings, blockers, and override governance
- Making a financial correction during review
- Marking a Beleg as paid

# Reviewing Belege in the review queue

The finance review queue is where you find **Belege** (receipt/expense documents) that are waiting for attention and act on them. This article shows how to filter the queue, read its warning and blocker counts, review a single Beleg, and run bulk actions. Viewing the queue requires the `belege:view` permission; acting on items requires `belege:transition`.

## Opening and filtering the review queue

The review queue lists Belege with summary information and lets you narrow the list by several dimensions:

- **Status** — e.g. Submitted, In Review, Approved.
- **Beleg type** — for example fuel, service, or another configured type.
- **Vehicle** or **driver**, or the **unassigned vehicle** / **unassigned driver** flags to surface receipts that are missing a link.
- **Cost center** (Kostenstelle) referenced by any line item.
- **Has warning** and **has blocker** — show only Belege that currently carry unresolved rule outcomes.

Results are ordered by Beleg date (newest first) and then by creation time. The queue is paginated; each page returns up to 100 items.

## Reading the queue columns

Each row summarizes one Beleg: its type, status, Beleg date, linked vehicle and driver, line-item count, total gross amount, and two governance counters — **warning count** and **blocker count**. These counters only include rule outcomes that have *not* been overridden, so a row's badges reflect what still needs resolving. Flags also mark a missing vehicle or missing driver.

Above the list, a summary shows the total number of matching Belege, how many carry at least one warning, and how many carry at least one blocker — a quick read on the health of the current filter.

## Reviewing a single Beleg

Open a Beleg from the queue to see its header, line items, and rule outcomes, then drive it through the workflow:

1. From **Submitted**, move the Beleg to **In Review** to pick it up. Blocking rule outcomes (for example a missing cost center or tax rate) must be resolved first.
2. While **In Review**, resolve any data problems, override warnings where justified, or apply a financial correction.
3. Finish the review by choosing **Approve**, **Reject**, or **Request clarification**. A clarification request requires a note.

> **Tip:** Use the *has blocker* filter to clear hard stops first, then the *has warning* filter to work through items that need a judgement call before approval.

## Acting on several Belege at once

Bulk actions let you apply the same transition to multiple selected Belege. The supported actions are **approve**, **reject**, **request clarification**, and **mark paid**.

| Bulk action | Allowed from state |
| --- | --- |
| Approve | In Review |
| Reject | In Review |
| Request clarification | In Review |
| Mark paid | Approved |

All selected Belege must be in the same state, and that state must be eligible for the chosen action — otherwise the whole batch is rejected before anything changes. Each item is then processed through the same governed transition service used for single actions, so every per-item rule gate, maker-checker check, and permission still applies. The result reports how many items were processed and lists any per-item errors.

## Related

- Understanding the Beleg approval workflow and states
- Approving a Beleg and how approval locks the record
- Rule warnings, blockers, and override governance
- Making a financial correction during review

# Rule warnings, blockers, and override governance

MobilityManager runs a configurable quality-rules engine over every **Beleg** (receipt/expense document) as it moves through review. This article explains the rules, their severities, where they block the workflow, and how an authorized reviewer overrides a warning with a governed, audited reason.

## The rules that are evaluated

Five rules are checked. Each produces an outcome with an explanation you can read on the Beleg:

| Rule | What it detects | Default severity |
| --- | --- | --- |
| Missing line-item cost center | A line item has no cost center (Kostenstelle) assigned. | Blocking |
| Missing line-item tax rate | A line item has no VAT rate (Umsatzsteuersatz). | Blocking |
| Fuel mismatch | A fuel Beleg's fuel type does not match the vehicle's registered fuel type. | Warning |
| Duplicate candidate | Another Beleg shares the same type, vehicle, date and gross amount (and invoice number when configured). | Warning |
| Mileage plausibility | The invoice mileage deviates from the vehicle odometer beyond the configured km or percentage threshold. | Warning |

Evaluation is deterministic for a given Beleg and configuration version, and results are cached until the Beleg changes, so re-opening a Beleg shows stable outcomes.

## Severities and what they mean

Each rule's severity is configurable per Beleg type and takes one of four values:

- **Off** — the rule is disabled and produces no outcome.
- **Info** — informational only.
- **Warning** — can be overridden with a reason.
- **Blocking** — a hard stop that must be fixed.

> **Note:** An unrecognized or mistyped severity value fails closed to Blocking, so a configuration typo can never silently switch off a quality gate.

## Where rules block the workflow

Rules are enforced at the review gates:

- **Blocking** outcomes prevent moving the Beleg to In Review and to Approved until the data is corrected. Missing cost center and missing tax rate specifically block the In Review step.
- **Warning** outcomes do not block reaching In Review, but they *must be overridden* before the Beleg can be Approved.

## Overriding a warning

Overriding lets an authorized reviewer accept a warning and proceed to approval. Overrides are governed:

1. Open the Beleg and locate the warning in its rule outcomes.
2. Choose to override it and enter a **reason**. The reason is mandatory — an empty reason is rejected.
3. The warning is marked as overridden and no longer blocks approval.

Only **Warning** outcomes can be overridden. Blockers cannot — you must fix the underlying data. A warning that is already overridden cannot be overridden again. Overriding requires the `belege:transition` permission.

## What gets recorded

Each override writes an immutable record capturing the rule, the reason, and the acting user and their email, plus an *override-warning* event in the audit trail. These records feed the compliance reporting dashboard, which shows warning and override trends over time so managers can spot patterns of frequent overrides.

## Related

- Approving a Beleg and how approval locks the record
- Reviewing Belege in the review queue
- Making a financial correction during review
- The compliance trail and retention

# How auto-assignment chooses a vehicle

In the auto-assign booking mode, a driver requests a time window and the system automatically picks the best available vehicle. This article explains how the vehicle is chosen, which strategies exist, and what can prevent an assignment.

## How a driver triggers auto-assign

The driver asks for a vehicle by supplying a **start time**, an **end time**, and optional **notes** — but no specific vehicle. The system finds a candidate and creates the booking in one step. Times are normalized to UTC, the end must be after the start, and the start cannot be in the past.

## Which vehicles are considered

The candidate set is narrowed in this order:

1. Vehicles in **pools the driver can access**.
2. Of those, vehicles that are **not already occupied** for the requested window.
3. Of those, vehicles that are **active**.

If no vehicle survives these filters, the driver is told no vehicles are available for the selected time.

## The two selection strategies

Your organization has one auto-assign strategy, applied to every auto-assign request. The default is **LeastUtilized**.

| Strategy | Primary sort | Tie-breaker |
| --- | --- | --- |
| **LeastUtilized** (default) | Lowest utilization percentage — current odometer against allowed annual mileage — so wear is spread evenly across the fleet. | Lowest cost per kilometre. |
| **LowestCost** | Lowest rate per kilometre (€/km), to minimize running cost. | Least utilized. |

> **Note:** Utilization and the €/km rate come from each vehicle's own record (the Fahrzeugakte, vehicle file). Keeping odometer readings, allowed annual mileage, and the per-kilometre rate up to date directly improves auto-assign decisions.

## Final checks before the vehicle is claimed

- **Licence enforcement:** if the driver does not hold the required driver's licence for the chosen vehicle and the organization runs licence enforcement in hard-block mode, the assignment is blocked (Führerscheinkontrolle, driver's licence check).
- **Approval gate:** if approval is required for auto-assigned bookings, the booking starts as **PendingApproval** instead of **Confirmed**. Otherwise it is confirmed immediately.

## Handling simultaneous requests

Two drivers can target the same free vehicle at the same instant. The booking is committed under a strict transaction with a final overlap re-check; if the slot was just taken, the system automatically retries (up to three attempts) and selects the next best vehicle. Only if nothing remains does the driver see "No vehicles available, please try again".

## Related

- Require approval for auto-assigned bookings
- Reassign or override a vehicle assignment
- Notifications sent on booking events
- The manager-assign booking workflow explained

# Reassign or override a vehicle assignment

Sometimes the wrong vehicle ends up on a booking — it is needed elsewhere, has gone in for service, or a better fit is free. This article explains how a fleet manager overrides an existing assignment and gets the driver onto a different vehicle.

## Choose the right path for the booking's state

There is no single "swap vehicle" button; how you override depends on where the booking is in its lifecycle. Because Cancelled and Rejected bookings never occupy a vehicle's slot, ending a booking always frees its vehicle immediately for reassignment.

| Current state | How to override |
| --- | --- |
| Request still **PendingManagerAssignment** | Nothing is assigned yet — simply pick a different vehicle from the available-vehicles list when you assign the request. |
| Auto-assigned booking still **PendingApproval** | Reject it with a reason. The slot is freed and the driver can run auto-assign again, which may select a different vehicle per your strategy. |
| Booking already **Confirmed** or **Active** | Cancel it as a fleet manager, then rebook on the correct vehicle (see below). |

## Override a confirmed booking

1. Open the booking that is on the wrong vehicle.
2. Use the fleet cancel action and enter a **reason** — a reason is required. This requires the `bookings:manage` permission.
3. The booking moves to **Cancelled** and the driver is automatically notified with your reason. The vehicle's slot is now free.
4. Create a replacement booking on the intended vehicle using the workflow your organization runs:
    - **Manager-assign:** ask the driver to resubmit the request, then assign the correct vehicle.
    - **Auto-assign:** have the driver run auto-assign again for the same window.
    - **Self-service:** the driver books the specific vehicle directly.

> **Warning:** Overriding a confirmed booking means cancelling it — there is no silent in-place vehicle change. Always give a clear reason so the driver understands why their vehicle changed, and confirm the replacement booking exists before considering the override complete.

> **Tip:** When reassigning to balance the fleet, check each candidate's utilization percentage. Moving a trip onto a less-used vehicle spreads wear and mirrors what the LeastUtilized auto-assign strategy would do.

## What the driver sees

The driver receives a cancellation notification with your reason. If you rebook through manager-assign, they then receive a fulfilment notification for the new vehicle; through auto-assign or self-service they receive a confirmation for the replacement booking.

## Related

- How auto-assignment chooses a vehicle
- Require approval for auto-assigned bookings
- Approve or reject a booking request
- Notifications sent on booking events

# Require approval for auto-assigned bookings

You can require a fleet manager to approve every automatically assigned booking before it becomes confirmed. This article explains the auto-assign approval gate, how to turn it on, and how approvers clear the resulting queue.

## What the approval gate does

Normally an auto-assigned booking is created as **Confirmed** immediately. When the auto-assign approval gate is on, the booking is instead created as **PendingApproval**: the vehicle and time slot are held, but the trip is not confirmed until an approver reviews it. Every user who holds the `bookings:approve` permission is notified that a booking needs approval.

## Turn the gate on or off

> **Note:** This is an organization-level setting, changed by a SystemAdmin in the company (tenant) settings. The "require approval for auto-assigned bookings" toggle is only relevant when your default booking mode is Auto-Assign.

1. Open your company settings.
2. In the booking settings, enable **require approval for auto-assigned bookings**.
3. Save. New auto-assigned bookings will now start in PendingApproval.

This gate is independent from the approval toggle for self-service bookings, so you can require approval for one path without the other. The rule fails closed: if the organization cannot be resolved for a request, approval is required rather than skipped.

## Clear the approval queue

Approvers work bookings that are waiting, not driver requests. Open the pending-approvals queue to see every booking with status **PendingApproval**, oldest first, with the driver, vehicle, and time window.

### Approve a booking

1. Open a pending booking and review the vehicle and window.
2. Approve it. The booking becomes **Confirmed**, you are recorded as the approver, and the driver is notified. Requires the `bookings:approve` permission.

### Reject a booking

1. Open the pending booking.
2. Reject it and enter a **reason** (required). The booking becomes **Rejected** and the driver is notified with the reason. Requires the `bookings:reject` permission.

> **Warning:** You cannot approve your own booking. Approval also re-checks availability at the moment you decide: if a confirmed booking has claimed the same vehicle and time in the meantime, approval is refused with "the vehicle is no longer available for this time period." A booking that has already been decided cannot be approved or rejected again.

> **Note:** Manager-assigned bookings never enter this queue. The fleet manager who assigns the vehicle is the approver, so those bookings are confirmed directly.

## Related

- How auto-assignment chooses a vehicle
- Approve or reject bookings awaiting approval
- Notifications sent on booking events
- Reassign or override a vehicle assignment

# Approve or reject a booking request

In the manager-assign workflow you approve a driver's booking request by assigning a vehicle to it, or you reject it with a reason. This article shows a fleet manager how to do both and what the system checks along the way.

> **Note:** Both actions require the `bookings:manage` permission (held by the FleetManager role) and only work while the request is still in the **PendingManagerAssignment** status.

## Approve by assigning a vehicle

1. Open the request from the approval queue and review the requested window and purpose.
2. Pick a vehicle from the available-vehicles list for that request.
3. Assign it. The system creates a booking with status **Confirmed**, moves the request to **Approved**, records you as the approver with a timestamp, and links the request to the new booking.

The driver is notified that their request was fulfilled, and the assigned vehicle's licence plate now appears on their request.

### Checks performed before the vehicle is assigned

| Check | Result if it fails |
| --- | --- |
| You are not the requesting driver | "You cannot assign a vehicle to your own booking request." |
| Vehicle exists, is active, and has a pool | Assignment is refused with the specific reason. |
| Driver has access to the vehicle's pool | "Driver does not have access to this vehicle's pool" |
| Vehicle is free for the window | Conflict: "Vehicle is not available for the requested time period" |
| Driver holds the required licence | Blocked when licence enforcement is in hard-block mode (Führerscheinkontrolle, driver's licence check) |

> **Warning:** Two managers can look at the same free vehicle at once. The assignment is committed under a strict transaction with a final availability re-check, so if someone books that vehicle a moment before you, you will get "Vehicle was just booked by another user. Please try again." Reopen the available-vehicles list and pick another vehicle.

## Reject a request

1. Open the request you cannot fulfil.
2. Choose to reject it and enter a **reason**. A reason is required — an empty reason is refused with "Rejection reason is required".
3. Confirm. The request moves to **Rejected**, and you are recorded as the rejecter with a timestamp.

The driver is notified that their request was declined, and the reason you entered is shown on the request so they understand why.

> **Tip:** Reject only when no vehicle can be provided. If a vehicle is simply unavailable at that exact time, it is often better to contact the driver and agree on a different window rather than reject outright.

## Related

- Review the manager approval queue
- The manager-assign booking workflow explained
- Notifications sent on booking events
- Reassign or override a vehicle assignment

# The manager-assign booking workflow explained

The manager-assign workflow lets drivers request a vehicle for a time window while a fleet manager decides which vehicle they get. This article explains how the workflow is structured, the roles involved, and the states a request moves through.

## Where manager-assign fits

MobilityManager supports three booking modes, set as your organization's default booking mode: **SelfService** (the driver picks a vehicle), **AutoAssign** (the system picks a vehicle), and **ManagerAssign** (a fleet manager picks the vehicle for the driver). Manager-assign is the mode described here.

## Who does what

| Role | Responsibility |
| --- | --- |
| **Driver** | Submits a request with a time window, a purpose, and optional notes. Does not choose a vehicle. |
| **FleetManager** (permission `bookings:manage`) | Reviews pending requests, sees which vehicles are available, and either assigns a vehicle or rejects the request with a reason. |

## The request lifecycle

1. **PendingManagerAssignment** — the driver has submitted the request; it is waiting in the manager's queue.
2. **Approved** — a manager assigned an available vehicle. This creates a real booking with status **Confirmed**, records who approved it and when, and links the request to the booking.
3. **Rejected** — a manager declined the request and supplied a reason. No booking is created.
4. **Cancelled** — the driver withdrew the request before it was assigned.

> **Note:** When a manager assigns a vehicle, the resulting booking's notes are prefixed with `[Manager-Assigned]` together with the driver's stated purpose, so the origin of the booking is always visible.

## How approval applies here

Manager-assigned bookings do **not** go through the separate approval queue used for self-service and auto-assigned bookings. The fleet manager who assigns the vehicle *is* the approver, so the resulting booking is Confirmed immediately.

### Segregation of duties

A fleet manager who is also the requesting driver cannot fulfil their own request. If the manager and the requester are the same person, assignment is refused with "You cannot assign a vehicle to your own booking request." This mirrors the self-approval guard used elsewhere and keeps request and approval in separate hands.

## Guards applied at assignment

- The vehicle must exist, be active, and belong to a pool.
- The driver must have access to that vehicle's pool.
- The vehicle must be free for the requested window (no overlapping booking).
- The driver must hold any required driver's licence (Führerscheinkontrolle, driver's licence check) when the organization runs licence enforcement in hard-block mode.

## Related

- Submit a booking request for manager approval
- Review the manager approval queue
- Approve or reject a booking request
- Require approval for auto-assigned bookings

# Notifications sent on booking events

MobilityManager sends in-app notifications at each step of the booking lifecycle so drivers and approvers never have to poll for updates. This article explains which event triggers which notification and who receives it.

## Where notifications appear

Notifications are delivered inside the application to the relevant user's notification list, where each one carries a title, a message, and a link to the related booking or request. Every booking-creation path — self-service, auto-assign, and manager-assign — routes through the same notification step, so the behavior is identical no matter how a booking was made. This single, shared notification step is why approvals no longer stall silently: whenever a booking needs review, the right people are told automatically.

## Notifications by event

| Event | Who is notified | Message |
| --- | --- | --- |
| Booking created and immediately Confirmed | The driver | Booking confirmed, with vehicle and time window. |
| Booking created but needs approval (PendingApproval) | Every user with the `bookings:approve` permission in the organization | "Booking Approval Required" — names the driver, vehicle, and window, asking them to review. |
| Approver approves a pending booking | The driver | Booking approved, with vehicle and time window. |
| Approver rejects a pending booking | The driver | Booking rejected, including the reason. |
| Manager assigns a vehicle to a driver's request | The driver | Request fulfilled — the booking is confirmed with the assigned vehicle. |
| Manager rejects a driver's request | The driver | Request rejected, including the reason, linking back to the requests list. |
| Fleet manager cancels a booking | The driver | Booking cancelled, including the reason. |

## How approvers are chosen

When a booking needs approval, the system looks at every user assigned to the organization and keeps only those who actually hold the `bookings:approve` permission. Each of them receives the "Booking Approval Required" notification, so approval never depends on a single named person being online.

> **Note:** Manager-assigned bookings do not generate an "approval required" notification, because the fleet manager who assigned the vehicle is already the approver. The driver receives the fulfilment notification directly.

> **Tip:** If drivers report that approvals "go quiet", check that at least one active user in the organization has the `bookings:approve` permission. With no eligible approver, no approval notification can be delivered.

## Reasons travel with the notification

For every event that ends a booking negatively — a rejected request, a rejected pending booking, or a fleet-manager cancellation — the reason entered by the approver or manager is included in the driver's notification. This keeps the driver informed without a separate email or phone call, and gives them the context they need to resubmit or rebook. Confirmation and approval notifications instead carry the vehicle name and the booking's start and end times so the driver can see exactly what was reserved.

## Related

- Approve or reject bookings awaiting approval
- Approve or reject a booking request
- Track and cancel your booking requests
- Require approval for auto-assigned bookings

# Review the manager approval queue

The manager approval queue is where fleet managers see every driver booking request that is waiting for a vehicle. This article shows how to open the queue and check which vehicles are available for a request.

> **Note:** Working the queue requires the `bookings:manage` permission, which the FleetManager role holds. Drivers without this permission cannot see other people's requests.

## Open the queue

Open the pending booking requests view. It lists every request in your organization whose status is **PendingManagerAssignment**, sorted by requested start time so the most urgent trips come first. Requests that are already Approved, Rejected, or Cancelled do not appear.

### What each request shows

| Column | Description |
| --- | --- |
| Driver | Name and email of the requesting driver. |
| Requested start / end | The time window the driver needs a vehicle. |
| Purpose | Why the driver needs the vehicle. |
| Created | When the request was submitted. |

## Check available vehicles for a request

Before assigning, open a request to see the vehicles you can actually give this driver. The available-vehicles list is filtered specifically for that request:

- Only vehicles in **pools the driver can access** are considered.
- Only **active** vehicles are shown.
- Vehicles already occupied by another booking during the requested window are removed, so every vehicle in the list is genuinely free for that time.

### Fields shown for each candidate vehicle

| Field | Use |
| --- | --- |
| Licence plate, VIN | Identify the vehicle. |
| Brand, model, year | Match the vehicle to the trip. |
| Pool | Which pool the vehicle belongs to. |
| Utilization % | How heavily the vehicle is already used (current odometer against allowed annual mileage), useful for spreading wear across the fleet. |

> **Tip:** If the available-vehicles list is empty, the driver may not have access to any pool with a free vehicle for that window. Widen the driver's pool access or ask them to adjust the requested time.

## Next step

Once you have picked a suitable vehicle, assign it to the request to create the confirmed booking, or reject the request with a reason. See the related article for both actions.

## Related

- Approve or reject a booking request
- The manager-assign booking workflow explained
- Approve or reject bookings awaiting approval
- Reassign or override a vehicle assignment

# Submit a booking request for manager approval

When your organization runs the manager-assign booking mode, you tell a fleet manager *when* you need a vehicle and *why*, and the manager picks the vehicle for you. This article shows a Driver how to submit that booking request.

## Before you start

- You need a driver profile in MobilityManager. If the system cannot find one for your account, submitting a request fails with "Driver profile not found for current user".
- You do not choose a vehicle. In this workflow the request holds only a time window and a purpose; a fleet manager assigns the actual vehicle later.

## Submit a booking request

1. Open the booking requests area and start a new request.
2. Enter the **requested start time** and **requested end time** for the period you need a vehicle.
3. Enter a **purpose**. This field is required and helps the manager decide which vehicle fits (for example, "Kundentermin München" or "Materialtransport Baustelle").
4. Optionally add **notes** with anything the manager should know (passengers, load, preferred pool).
5. Submit the request.

On success the request is created with the status **PendingManagerAssignment** and you receive a request number. No vehicle is reserved yet.

## Rules that are enforced

| Rule | What happens if it fails |
| --- | --- |
| Both start and end time are required | "Start time and end time are required" |
| End time must be after start time | "End time must be after start time" |
| Start time cannot be in the past | "Cannot create request in the past" |
| Purpose must not be empty | "Purpose is required" |

> **Note:** Times are stored in UTC. Enter them in your local working time as usual; the system normalizes them so the "not in the past" check and the reserved window are correct regardless of your device's time zone.

## What happens next

1. Your request appears in the fleet manager's approval queue, sorted by requested start time.
2. A manager reviews it and either assigns an available vehicle (your request becomes **Approved** and a confirmed booking is created) or rejects it with a reason.
3. You are notified of the outcome, and the assigned vehicle's licence plate appears on your request.

> **Tip:** While a request is still PendingManagerAssignment you can cancel it yourself. Once it has been approved, the request is closed and you would instead cancel the resulting booking.

## Related

- Track and cancel your booking requests
- The manager-assign booking workflow explained
- Notifications sent on booking events
- How auto-assignment chooses a vehicle

# Track and cancel your booking requests

After you submit a booking request for manager approval, you can follow its progress and cancel it while it is still waiting. This article shows a Driver how to review request status and cancel a pending request.

## View your requests

Open the booking requests area to see all requests you have submitted, newest first. Each row shows the requested time window, the purpose, the current status, and — once assigned — the vehicle's licence plate.

### Request statuses

| Status | Meaning |
| --- | --- |
| **PendingManagerAssignment** | Waiting for a fleet manager to assign a vehicle or reject the request. |
| **Approved** | A manager assigned a vehicle; a confirmed booking now exists for you. |
| **Rejected** | A manager declined the request. The rejection reason is shown. |
| **Cancelled** | You cancelled the request before it was assigned. |

## Open a request for details

Select a request to see its full detail, including your notes, and — once approved — the assigned vehicle's brand, model, licence plate, VIN, and pool, plus who approved it and when. If the request was rejected, the reason appears here. An approved request also links to the resulting booking so you can open it directly.

> **Note:** You can only open requests that belong to you. Fleet managers with the manage-bookings permission can also view any request in their organization.

## Cancel a pending request

1. Open the request you want to withdraw.
2. Choose to cancel it and optionally enter a reason (for example, "trip no longer needed").
3. Confirm. The request moves to **Cancelled** and the time it was cancelled is recorded.

### When cancelling is allowed

- You can cancel **only your own** requests. Trying to cancel someone else's request is refused.
- You can cancel **only while the status is PendingManagerAssignment**. If the request is already Approved, Rejected, or Cancelled, cancelling is refused with a message naming the current status.

> **Tip:** If your request has already been approved and you no longer need the vehicle, cancel the resulting booking instead of the request. Cancelling the booking frees the vehicle for other drivers.

## Related

- Submit a booking request for manager approval
- The manager-assign booking workflow explained
- Notifications sent on booking events
- Reassign or override a vehicle assignment

# Understanding booking conflicts and vehicle availability

MobilityManager guarantees that a vehicle can only be reserved by one driver at a time. This article explains how availability is calculated, why some bookings block a slot and others do not, and how the system prevents double-booking.

## What "available" means

When you search for a vehicle for a given window, the system returns a vehicle only if no other booking occupies that vehicle for any part of your requested period. Availability is computed on the exact start and end instants you choose, using the same values that will be stored if you book — so what you see as free is genuinely reservable.

## Which bookings block a slot

Not every booking counts against availability. Only bookings in an **occupying** status hold a vehicle's time slot:

| Status | Blocks the slot? |
| --- | --- |
| Pending / PendingApproval | Yes — a booking awaiting approval still reserves the window. |
| Confirmed | Yes |
| Active | Yes |
| Completed | Yes (historical use of that window) |
| Cancelled | No — the window is released. |
| Rejected | No — never occupied the vehicle. |

> **Note:** Cancelled and Rejected bookings deliberately do not block availability. As soon as a booking is cancelled or rejected, the vehicle is immediately bookable again for that period.

## How overlap is detected

Two bookings overlap when one starts before the other ends *and* ends after the other starts. The rule is a **half-open interval**: a booking occupies its window from the start time up to, but not including, the end time.

The practical effect is that **back-to-back bookings are allowed**. If one booking ends at 12:00 and another starts at 12:00 on the same vehicle, they are not considered to overlap. Any actual time overlap, however, is treated as a conflict.

## What happens when there is a conflict

If you try to book a vehicle whose window overlaps an existing occupying booking, the booking is refused and you see "Vehicle not available for selected time". Pick a different vehicle, or adjust your times so they no longer overlap.

## Preventing double-booking under load

Availability is re-checked at the moment you commit the booking, not just when you searched. This closes the small gap where two people could both see the same vehicle as free and try to book it at once:

- The booking is written inside a strict, serialized transaction that re-verifies there is still no overlap immediately before saving.
- If another booking has claimed the window in the meantime, your booking is rolled back and returned as a conflict — for example "Vehicle was just booked by another user. Please try again."

In that case, simply search again and choose another vehicle or time.

> **Tip:** If you frequently hit conflicts on a popular vehicle, widen your search to other vehicles in your accessible pools, or try adjacent time windows — back-to-back slots are valid and often free.

## Availability and your access

Availability is always evaluated within the vehicles you are allowed to book. A vehicle that is inactive, has no pool, or sits in a pool you cannot access will never appear as available to you, regardless of whether its time slot is free.

## Related

- Choosing start and end times, and how times are handled
- Book a vehicle yourself (self-service)
- The booking calendar view
- Which vehicles and pools you can book

# The booking calendar view

The booking calendar gives fleet managers a scheduler-style overview of who has which vehicle and when. This article explains what the calendar shows, how to filter it, and the permission you need to use it.

## Who can use the calendar

The calendar views require the **bookings:view-all** permission, which the **FleetManager** role includes. Drivers, who only hold view-own, do not have the fleet calendar and work from their personal bookings list instead.

## What the calendar shows

The calendar displays bookings across the fleet as time blocks against each vehicle. For a selected date range it returns every booking whose window overlaps that range. Each entry carries:

- the vehicle, shown as "licence plate – brand model", plus its pool;
- the driver who holds the booking;
- the planned start and end times;
- the booking status; and
- any notes on the booking.

> **Note:** The calendar data includes bookings of every status. The scheduler hides Cancelled and Rejected bookings from the visual timeline, while the underlying all-bookings table keeps them so you can still filter and see them there.

## Choosing the date range

You can set a start date and an end date for the view. If you do not specify them, the calendar defaults to a window that begins about a week in the past and extends roughly a month ahead, so recent and upcoming bookings are visible at a glance. The end date must be later than the start date.

## Filtering by pool

You can filter the calendar to a single vehicle pool. The pool filter list shows each active pool along with how many active vehicles it contains and whether it is a shared pool. Selecting a pool narrows both the vehicles shown and the bookings displayed to that pool.

> **Tip:** When you filter by a specific pool, vehicles from your organization's designated common pool are still included alongside it, so shared vehicles remain visible no matter which pool you focus on.

## The vehicle resource list

Alongside bookings, the calendar can load the fleet's bookable vehicles as calendar resources — active vehicles that are assigned to a pool. Each vehicle appears with its licence plate, brand, model, pool, and colour, which is useful for colour-coding rows in the scheduler.

## Per-vehicle calendar

You can also open the bookings for a single vehicle — for example from a vehicle's detail page — over a chosen date range. This shows the same booking information filtered to that one vehicle, which is helpful when checking a specific car's schedule before making a change or planning maintenance. If the vehicle does not exist, you will get a "Vehicle not found" result.

## Reading conflicts on the calendar

Because occupying bookings block a vehicle's slot, two active or upcoming bookings should never overlap on the same vehicle. If you are arranging a manager-driven change, use the calendar to spot free gaps — remember that back-to-back bookings (one ending exactly when the next begins) are allowed and are not a conflict.

## Related

- Understanding booking conflicts and vehicle availability
- What the Driver role can book
- Which vehicles and pools you can book
- Viewing your own bookings

# Book a vehicle yourself (self-service)

Self-service booking lets you reserve a fleet vehicle for a specific time window without going through a fleet manager. This article walks you through finding an available vehicle and creating the booking.

## Before you start

Self-service booking requires the **bookings:create** permission, which the built-in **Driver** role includes. You can only book vehicles that belong to a vehicle pool (Fahrzeug-Pool) you have access to, and the vehicle must be active. If your organization requires it, your booking may need manager approval before it is confirmed (see the Related articles).

## Step 1: Search for an available vehicle

Open the self-service booking area and enter the **start time** and **end time** you need the vehicle for. The system returns only vehicles that are:

- in a pool you are allowed to book from;
- marked active; and
- not already occupied by another booking that overlaps your window.

Each result shows the licence plate (Kennzeichen), brand and model, model year, colour, and the pool the vehicle belongs to. If nothing appears, either no vehicle in your pools is free for that window, or you are not assigned to any pool yet.

> **Tip:** The end time must be later than the start time. If you submit an end time that is the same as or earlier than the start, you will see "End time must be after start time".

## Step 2: Create the booking

1. Select the vehicle you want from the availability list.
2. Confirm your start and end times.
3. Optionally add **notes** (for example, the purpose of the trip). Notes are free text and optional.
4. Submit the request.

When the booking is created you receive a booking ID and its initial status.

## What happens after you submit

The system validates the request before saving it. A booking is created only if all of the following hold:

| Check | What it means |
| --- | --- |
| Time order | The start time must be before the end time. |
| Not in the past | The start time cannot be earlier than now — you cannot back-date a booking. |
| Vehicle valid | The vehicle must exist, be active, and be assigned to a pool. |
| Pool access | You must have access to the vehicle's pool, otherwise the request is refused. |
| Availability | No other active or upcoming booking may overlap your window. |
| Driver licence | If licence enforcement is set to hard block, you must hold the required driving licence class for that vehicle. |

If your organization does not require approval for self-service bookings, the booking is created as **Confirmed** and the vehicle is reserved immediately. If approval is required, it is created as **PendingApproval** and a manager must approve it before it becomes Confirmed.

> **Note:** If someone books the same vehicle for an overlapping time in the moment you submit, you will get a conflict message such as "Vehicle not available for selected time" or "Vehicle was just booked by another user. Please try again." Simply pick another vehicle or time and retry.

## Related

- Choosing start and end times, and how times are handled
- Which vehicles and pools you can book
- Understanding booking conflicts and vehicle availability
- Viewing your own bookings

# Cancelling a booking

If your plans change, you can cancel a booking you no longer need so the vehicle is freed for other drivers. This article explains when cancellation is allowed, how to do it, and what happens afterward.

## Who can cancel

You can cancel **your own** bookings. Attempting to cancel a booking that belongs to another driver is refused with "You can only cancel your own bookings". A fleet manager can additionally cancel any booking in the fleet (covered separately in the Related articles).

## When cancellation is allowed

Cancellation is only possible for bookings that have not yet been used and have not already ended:

| Current status | Can you cancel? |
| --- | --- |
| PendingApproval | Yes — at any time before it is approved. |
| Confirmed | Yes — but only before the start time is reached. |
| Active | No — the trip has already started. |
| Completed | No — the trip is finished. |
| Cancelled | No — it is already cancelled. |
| Rejected | No — a rejected booking cannot be cancelled. |

The key difference between the two cancellable states: a **PendingApproval** booking can be cancelled up until a manager acts on it, while a **Confirmed** booking can only be cancelled while it is still in the future. Once a Confirmed booking's start time has passed, you will see "Cannot cancel: booking already started".

## How to cancel

1. Open the booking from your bookings list.
2. Choose to cancel it.
3. Optionally enter a **reason** for the cancellation. The reason is free text and is not required, but it is stored with the booking for reference.
4. Confirm.

The booking's status changes to **Cancelled**, and the time and — if you provided one — the reason are recorded against it. The vehicle's time slot is released immediately, so the vehicle becomes available for that window again.

> **Note:** A cancelled booking no longer blocks availability. Cancelled and Rejected bookings are treated as non-occupying, so the vehicle can be booked again for the same period right away.

## After you cancel

The cancelled booking stays in your bookings history with the Cancelled status so there is a record of it — cancelling does not delete it. You can review when it was cancelled and the reason from the booking's detail view.

> **Warning:** Cancellation cannot be undone. If you still need the vehicle, create a new booking rather than trying to reverse the cancellation. If the same window is still free, you can rebook the same vehicle immediately.

## If a manager cancels your booking

A fleet manager can cancel any booking that has not started or completed, and must provide a reason when they do. When that happens, you are notified of the cancellation and the reason, so check your notifications if a booking you expected disappears from your active list.

## Related

- Viewing your own bookings
- Book a vehicle yourself (self-service)
- Understanding booking conflicts and vehicle availability
- Starting and ending a trip

# Choosing start and end times, and how times are handled

Every booking is defined by a start time and an end time. This article explains the rules those times must follow and how MobilityManager interprets and stores them so your reservation behaves predictably.

## The basic rules

- The **end time must be after the start time**. Equal or reversed times are rejected with "End time must be after start time".
- The **start time cannot be in the past**. When you create a booking, the start must be now or later, so you cannot back-date a reservation ("Cannot create booking in the past").
- The window you choose is the window that is reserved — the vehicle is held for the whole period from start to end.

## How times are normalized

Internally, all booking times are stored and compared in **UTC (Coordinated Universal Time)**. The web app sends the times you pick as UTC timestamps, and the server keeps them that way. This matters for two reasons:

- Availability and overlap checks are always performed on the same UTC instants that get stored, so what you see as "free" is exactly what gets reserved.
- The "cannot book in the past" check compares your start time against the current UTC time, so it behaves consistently regardless of where you or the vehicle are located.

You do not need to convert anything yourself — pick the local date and time you want in the interface, and the app handles the conversion. Just be aware that displayed times reflect this underlying UTC value.

> **Note:** Because the check is against the actual current time, avoid setting a start time that is only seconds away. Give yourself a small buffer so the request does not slip into the past between selecting and submitting.

## Back-to-back bookings are allowed

Availability uses a **half-open interval** rule: a booking occupies its slot from the start time up to (but not including) the end time. Two bookings where one ends exactly when the next begins are *not* treated as overlapping.

For example, if a colleague has a vehicle booked until 12:00, you can book the same vehicle starting at 12:00 without a conflict. If you instead tried to start at 11:59, the windows would overlap and your booking would be refused.

| Existing booking | Your requested window | Result |
| --- | --- | --- |
| 09:00 – 12:00 | 12:00 – 14:00 | Allowed (back-to-back) |
| 09:00 – 12:00 | 11:30 – 13:00 | Conflict (overlap) |
| 09:00 – 12:00 | 08:00 – 09:00 | Allowed (ends when the other starts) |

> **Tip:** If you plan to keep a vehicle longer than you first booked, and the next slot is free, extend by cancelling and rebooking, or ask a fleet manager. There is no partial "trim" of an existing window from the self-service screen.

## Choosing a realistic window

Reserve the full period you actually expect to need the vehicle, including buffer for return. Over-reserving blocks the vehicle for others; under-reserving may leave you without a valid booking if your trip runs long. When you later start and end the trip, the system records the actual times separately from your planned window.

## Related

- Book a vehicle yourself (self-service)
- Understanding booking conflicts and vehicle availability
- Starting and ending a trip
- Cancelling a booking

# Starting and ending a trip

A confirmed booking is reserved for you, but the actual trip is tracked separately by starting and ending it. This article explains how to start a booking, record the return odometer when you finish, and what the system captures along the way.

## Starting your trip

When it is time to collect the vehicle, start your booking. Only the booking owner can start it, and a few conditions apply:

- The booking must be in **Confirmed** status. You cannot start a PendingApproval, Cancelled, or already-Active booking.
- You can start up to **30 minutes before** the scheduled start time. Try earlier than that and you will see "Cannot start booking yet. Please wait until closer to the scheduled start time."

When you start, the system:

1. records the **actual start time** (the current moment);
2. captures the vehicle's current odometer reading as the **start odometer**; and
3. changes the status to **Active**.

> **Note:** The start odometer is taken automatically from the vehicle's recorded odometer — you do not enter it. This becomes the baseline for calculating trip mileage.

## Ending your trip

When you return the vehicle, end the booking and enter the current odometer reading. Only the booking owner can end it. You can end a booking that is **Active**, or one that is still **Confirmed** if you never explicitly started it (as long as its scheduled window has begun — the same 30-minute rule applies).

1. Open the booking and choose to end it.
2. Enter the vehicle's current **end odometer** reading.
3. Submit.

The reading you enter is validated to keep mileage data clean:

| Rule | Message if violated |
| --- | --- |
| Cannot be negative | "Odometer reading cannot be negative" |
| Not less than the start odometer | "Odometer reading cannot be less than start odometer (…)" |
| Not less than the vehicle's last known reading | "Odometer reading cannot be less than previous reading (…)" |

## What ending records

When the reading passes validation, the system:

- sets the status to **Completed** and records the **actual return time**;
- stores your **end odometer** and calculates the **trip mileage** (end minus start);
- updates the **vehicle's current odometer** to your new reading, so the next driver's trip starts from the correct baseline.

> **Warning:** Enter the odometer carefully. Your reading becomes the vehicle's new official odometer value, and the next booking's start odometer is taken from it. An inflated reading will roll forward to every subsequent trip.

## Ending without starting

If you complete a booking directly from Confirmed without having started it, the system records the completion time as the actual start as well, and uses the vehicle's odometer at that moment as the start reading, so the mileage still reflects the distance driven rather than a future-dated window.

## Related

- Viewing your own bookings
- Book a vehicle yourself (self-service)
- Cancelling a booking
- Choosing start and end times, and how times are handled

# Viewing your own bookings

You can review all of your reservations — upcoming, active, and past — from your bookings list. This article explains what the list shows, how to open a single booking's details, and what each status means.

## Opening your bookings list

Your bookings area shows every booking that belongs to you, both future and historical, sorted with the most recent start time first. Viewing your own bookings requires the **bookings:view-own** permission, which the **Driver** role includes. You only ever see your own bookings here; other drivers' reservations are not listed.

Each row in the list shows:

- the vehicle's licence plate (Kennzeichen), brand, and model;
- the planned start and end times;
- the current status; and
- the date the booking was created.

## Booking statuses

A booking moves through a defined lifecycle. The status tells you where it stands:

| Status | Meaning |
| --- | --- |
| PendingApproval | Created, but waiting for a manager to approve it before it is reserved. |
| Confirmed | Approved (or auto-confirmed) — the vehicle is reserved for you. |
| Active | You have started the trip; the vehicle is in use. |
| Completed | You have ended the trip and recorded the return. |
| Cancelled | The booking was cancelled by you or a fleet manager. |
| Rejected | A manager declined the booking during approval. |

## Opening a single booking

Select any booking to open its full detail view. In addition to the summary fields, the detail view includes:

- vehicle identity: VIN (Fahrzeug-Identifizierungsnummer), model year, and colour;
- the pool the vehicle belongs to and any pickup instructions (drawn from the pool's description);
- your planned window plus the **actual start time** and **actual return time** once you begin and end the trip;
- the **start odometer**, **end odometer**, and calculated **trip mileage** (kilometres driven);
- your notes; and
- audit details where relevant — when it was cancelled and why, or when it was approved or rejected and the rejection reason.

> **Note:** You can only open the details of a booking that belongs to you. Trying to open someone else's booking returns "Booking not found".

## Reading trip mileage

Trip mileage is shown only after both an odometer reading at the start and one at the end exist. It is simply the end odometer minus the start odometer. If a trip has not been ended yet, the mileage field stays empty.

## Acting on a booking

From a booking you can take the actions its status allows — for example cancel it while it is still upcoming, start the trip when the time arrives, or end the trip and record the odometer. Which actions are available depends on the current status; see the Related articles for each action.

> **Tip:** If a booking you expected to be Confirmed still shows PendingApproval, your organization requires manager approval for self-service bookings. It will move to Confirmed once a manager approves, or to Rejected with a reason if declined.

## Related

- Book a vehicle yourself (self-service)
- Starting and ending a trip
- Cancelling a booking
- What the Driver role can book

# What the Driver role can book

The built-in **Driver** role is the self-service end-user role in MobilityManager. This article explains exactly what a Driver can book and do with bookings, and where the boundaries are compared with a fleet manager.

## What the Driver role includes

Permissions are enforced by the platform, and the Driver role is granted a deliberately focused set for booking and driving. In the booking area, a Driver can:

- **Create bookings** for themselves (permission `bookings:create`);
- **View their own bookings**, upcoming and past (permission `bookings:view-own`);
- **View vehicles** so they can pick one to book (permission `vehicles:view`);
- manage their own driving licence: **view their licence details** and **upload a licence verification** (permissions `driver-license:view` and `license-verification:upload`).

## What a Driver can actually do end to end

1. Search for an available vehicle in the pools they can access and create a booking.
2. See all of their own bookings and open any one for full details.
3. Cancel their own booking while it is still PendingApproval or a not-yet-started Confirmed booking.
4. Start their own booking (recording the vehicle's odometer) and end it (recording the return odometer).

Every one of these actions is scoped to the driver: a Driver acts only on their own bookings. Opening or cancelling another driver's booking is refused.

## What a Driver cannot do

Several booking capabilities are reserved for the **FleetManager** role (or a custom role granted the matching permissions):

| Capability | Required permission | Driver? |
| --- | --- | --- |
| See the fleet-wide booking calendar | bookings:view-all | No |
| Approve a pending booking | bookings:approve | No |
| Reject a pending booking | bookings:reject | No |
| Cancel another person's booking | bookings:manage | No |
| View bookings awaiting approval | bookings:view-pending | No |

> **Note:** Because a Driver does not hold `bookings:view-all`, the fleet booking calendar and the per-vehicle and per-pool calendar views are not available to the Driver role. Drivers work from their own bookings list instead.

## Access still depends on pools and licences

Holding `bookings:create` does not by itself let you book any vehicle. You can only book from pools you have access to (shared pools plus any a fleet manager has assigned you), and only vehicles that are active and assigned to a pool. If driver-licence enforcement is set to hard block, you must hold the required licence class for a vehicle before you can book it.

> **Tip:** The Driver role intentionally does not include finance or admin areas such as expenses (Belege). If you need access to modules beyond self-service booking and your own licence, a SystemAdmin can assign you a role with the appropriate permissions.

## Custom roles

Beyond Driver, FleetManager, and SystemAdmin, your organization can define custom roles with any combination of these permissions. If your access differs from what is described here, your account may be on a custom role — check with a SystemAdmin.

## Related

- Which vehicles and pools you can book
- Book a vehicle yourself (self-service)
- Viewing your own bookings
- The booking calendar view

# Which vehicles and pools you can book

Self-service booking only ever shows you the part of the fleet you are entitled to use. This article explains how vehicle pools (Fahrzeug-Pools) determine which vehicles you can book, and why a vehicle might not appear in your availability list.

## Pools decide access

Vehicles are organized into **pools**, and your access is granted at the pool level, not per vehicle. When you search for an available vehicle, the system first works out which pools you can access, then offers you the eligible vehicles inside those pools. You get access to a pool in one of two ways:

- **Shared pools:** a pool flagged as a shared pool (Gemeinschafts-Pool) is automatically available to every driver in your organization — no explicit assignment is needed.
- **Assigned pools:** a fleet manager can explicitly assign you to additional, non-shared pools. Those vehicles then appear for you too.

There is no automatic access to a non-shared pool: unless a pool is shared or you have been assigned to it, its vehicles will not appear in your search.

## The common pool

Each organization can designate one pool as the **common pool**. It behaves like any other pool for access purposes — you must either have it shared with you or be assigned to it — but fleet managers see its vehicles across calendar views by default. If you are unsure whether you can book from the common pool, check your accessible pools or ask a fleet manager to assign you.

## Which vehicles show up for a given window

Within the pools you can access, a vehicle only appears as bookable when all of these are true:

| Condition | Why it matters |
| --- | --- |
| Vehicle is active | Decommissioned or inactive vehicles are never offered. |
| Vehicle is assigned to a pool | A vehicle with no pool cannot be self-service booked. |
| No overlapping booking | The vehicle must be free for your entire requested window. |
| Licence requirements met | If enforced, you must hold the required driving licence class for that vehicle. |

> **Note:** Each available vehicle is listed with its licence plate, brand and model, year, colour, and the name of the pool it comes from, so you can tell which pool a vehicle belongs to before you book.

## Why a vehicle you expected is missing

- **You are not in any pool.** If your availability list is always empty, you may not be assigned to any pool and there may be no shared pool. Ask a fleet manager to assign you.
- **The vehicle is already booked** for part of your window by someone else.
- **The vehicle is inactive** or has been removed from its pool.
- **A licence requirement blocks you** for that specific vehicle.

> **Tip:** If you legitimately need a vehicle from a pool you cannot see, contact a fleet manager. Only they can assign drivers to pools; you cannot grant yourself access.

## Booking is always for you

Self-service bookings are created for the signed-in driver — you book from the fleet you can access, for your own use. If a vehicle needs to be reserved for someone else, a fleet manager handles that through the manager-assignment workflow rather than self-service.

## Related

- Book a vehicle yourself (self-service)
- What the Driver role can book
- Understanding booking conflicts and vehicle availability
- Viewing your own bookings

# Record communications for a partner or contact

This article explains the communications log in the CRM module — the list of phone numbers, email addresses, and other contact channels you keep for a partner or a contact. Recording communications requires the `crm:manage` permission.

## What a communications log is

Each business partner has its own communications log, and each contact person has a separate one of their own. An entry is a single channel: a phone number, an email address, a website, and so on. This keeps a partner's general switchboard number distinct from an individual contact's direct line.

## Communication types

Every entry must use one of these fixed types:

| Type | Typical value |
| --- | --- |
| Phone | Landline or switchboard number |
| Mobile | Mobile / cell number |
| Fax | Fax number |
| Email | Email address |
| Website | Web address |
| Other | Any channel that does not fit the above |

## Fields on each entry

| Field | Required | Notes |
| --- | --- | --- |
| Type | Yes | One of the six types above |
| Value | Yes | The number, address, or URL; up to 256 characters |
| Label | No | Optional description, e.g. "Claims hotline"; up to 100 characters |
| Primary | No | Marks the preferred entry for its type |
| Sort order | No | Controls display position; assigned automatically if left blank |

## Add communications

1. Open the **CRM** module and go to the partner's detail page.
2. Open the communications log for the partner, or for a specific contact.
3. Add an entry, choose its type, and enter the value.
4. Add an optional label to explain what the channel is for.
5. Mark one entry per type as **primary** if you want it treated as the preferred channel.
6. Save. The change is recorded in the CRM audit trail as a communications update.

## Primary channels and why they matter

You can mark one entry as primary *per type* — for example one primary Phone and one primary Email. The partner list uses these to show a headline phone number and email for each partner: it picks the primary phone or mobile, and the primary email, falling back to the first available entry. Marking the right primaries keeps those list columns and CSV exports accurate.

> **Warning:** You cannot mark two entries of the same type as primary. Saving two primary phone numbers, for instance, returns "Only one primary communication is allowed for type Phone." Leave just one primary per type.

## Partner-level vs contact-level

Record general, organization-wide channels (main switchboard, info email, company website) on the **partner**. Record a person's direct line and personal work email on the **contact**. Both logs behave the same way and follow the same type and primary rules.

> **Note:** Saving the communications log replaces the entire list for that partner or contact in one operation. To learn how editing, removing, and reordering entries works, see "Update the communications log."

## Related

- Update the communications log
- Add contacts to a partner
- Manage contact details
- The CRM dashboard

# The CRM dashboard

The CRM dashboard gives you an at-a-glance overview of your partner and contact data — how many partners you have, how they break down by type and country, and where contact coverage is thin. This article explains each panel. Viewing the dashboard requires the `crm:view` permission.

## Opening the dashboard

Open the **CRM** module and select **Dashboard**. The figures are computed live from your tenant's current data each time you open it.

## Summary figures

The summary strip reports the headline counts:

| Figure | Meaning |
| --- | --- |
| Total partners | All partners, active and inactive |
| Active / inactive partners | Split of total partners by active status |
| Suppliers | Partners of type Supplier |
| Insurances | Partners of type Insurance |
| Total contacts | Count of *active* contacts across all partners |
| Total communications | All communication entries, combining partner-level and contact-level logs |

## Partner type breakdown

A breakdown of partners grouped by type (Supplier and Insurance), ordered with the largest group first. This confirms the balance of your vendor base versus your insurers.

## Contact coverage

Coverage highlights partners that may need attention:

- **Partners with contacts** and **partners without contacts** — how many partners have at least one active contact person.
- **Partners with a primary contact** and **partners without a primary contact** — how many have a designated main point of contact.

> **Tip:** A high "without a primary contact" figure is a to-do list. Open those partners and mark a main contact so your team always knows who to call.

## Geographic breakdown

Partners grouped by country, showing the top ten, largest first. Partners with no country recorded are grouped under **Unspecified** — a prompt to fill in missing addresses.

## Communication coverage

A count of communication entries by type (Phone, Mobile, Fax, Email, Website, Other) across both partner and contact logs, largest first. It shows which channels you capture most and where data may be missing.

## Top partners by contacts

The ten partners with the most active contacts, largest first. These tend to be your most involved relationships — large leasing providers or insurers with several handlers.

## Recent partners

The ten most recently created partners, newest first, with type, city, country, active contact count, and creation date. Use it to confirm that partners added recently have been fully filled in.

## Reading the figures correctly

> **Note:** Contact-based figures (total contacts, contact coverage, top partners by contacts) count only *active* contacts, while partner totals include inactive partners split out separately. The dashboard also reports how long its query took to run, which is a diagnostic detail rather than a business metric.

## Related

- Search partners and contacts
- Create a business partner
- Manage contact details
- What the CRM module does for your fleet

# Update the communications log

This article explains how updating a communications log works in the CRM module, including the important "replace-all" behavior. Understanding it prevents accidentally losing entries. Updating communications requires the `crm:manage` permission.

## Saving replaces the whole log

When you save a partner's or a contact's communications, the system does not merge your changes into the existing entries — it **replaces the entire list** with exactly what you submit. Any existing entry that is not part of the saved set is removed.

> **Warning:** Always work from the full, current list when updating. If you save only the one new number you meant to add, every other entry for that partner or contact is deleted. Keep the entries you want to retain in the list before saving.

## Edit an existing entry

1. Open the **CRM** module and go to the partner detail page, then open the communications log for the partner or the contact.
2. Change the value, label, type, primary flag, or order of any entry.
3. Keep every entry you want to preserve in the list.
4. Save. The complete list is written, an updated timestamp is set, and the change is recorded in the CRM audit trail.

## Remove an entry

To delete a channel, remove it from the list and save. Because the save replaces the whole log, the removed entry is gone once you save the remaining set. Saving an empty list clears all communications for that partner or contact.

## Reorder entries

Entries are shown in ascending **sort order**. To change the sequence, set the sort-order values to reflect the order you want, then save.

- Entries are stored and returned sorted by their sort-order value.
- If you leave a sort order at zero or below, the system assigns one automatically based on the entry's position in the list, so the order you enter them in is preserved.

## Rules enforced on save

| Rule | What happens if broken |
| --- | --- |
| Each entry needs a value | Save is rejected: "Communication value is required." |
| Type must be Phone, Mobile, Fax, Email, Website, or Other | Save is rejected: "CommunicationType is invalid." |
| At most one primary entry per type | Save is rejected: "Only one primary communication is allowed for type …" |

Values and labels are trimmed automatically, and empty labels are stored as blank.

## Partner and contact logs are independent

Replacing a partner's communications never touches its contacts' communications, and vice versa. Each log is updated on its own. Editing a partner's or contact's other details (name, address, and so on) is also a separate action and does not change the communications log.

> **Tip:** Before a big cleanup, note the entries you plan to keep. Then edit, reorder, and remove in one pass and save once — a single save writes the final list and produces one clean audit entry.

## Related

- Record communications for a partner or contact
- Manage contact details
- Edit or deactivate a partner
- The CRM dashboard

# Add contacts to a partner

This article shows you how to add contact people (Ansprechpartner) to a business partner, so you always know who to call at your leasing company, workshop, or insurer. Adding contacts requires the `crm:manage` permission.

## How contacts relate to partners

A contact always belongs to exactly one partner. Open the partner first, then add its people. Each contact can carry its own communications (direct phone, mobile, email), separate from the partner's general communications.

## Add a contact

1. Open the **CRM** module and go to the partner's detail page.
2. Add a new contact.
3. Enter the **first name** and **last name** — both are required.
4. Optionally add a salutation, position, department, and notes.
5. Mark the person as the **primary contact** if they are your main point of contact.
6. Save. The contact is created as active, timestamped, and the action is recorded in the CRM audit trail.

## Contact field reference

| Field | Required | Notes / limit |
| --- | --- | --- |
| Salutation (Anrede) | No | Up to 50 characters, e.g. Herr, Frau, Dr. |
| First name | Yes | Up to 100 characters |
| Last name | Yes | Up to 100 characters |
| Position | No | Up to 100 characters, e.g. Fleet Account Manager |
| Department (Abteilung) | No | Up to 100 characters |
| Notes | No | Up to 1000 characters |
| Primary contact | No | Marks the main person for this partner |

## The primary contact

Marking someone as the primary contact promotes them to the top of the partner's contact list. In list and detail views, contacts are ordered with the primary contact first, then alphabetically by last name and first name.

> **Tip:** A partner can technically have more than one contact flagged as primary, but keeping a single clear primary contact per partner makes the record easier for the whole team to use. Update the flag on the previous person when your main contact changes.

## Adding several contacts

Repeat the steps above for each person you deal with — for example an account manager, a claims handler at an insurer, and a service advisor at a workshop. There is no uniqueness rule on contacts, so you can record two people with the same name if needed; use the position or department fields to tell them apart.

## After adding a contact

Once a contact exists, open it to add their direct communications (phone, mobile, email, and so on). Communications are maintained per contact and are separate from the partner-level communications log.

> **Note:** If you try to save a contact without a first or last name, the system returns "FirstName is required" and/or "LastName is required." Fill in both names to continue.

## Related

- Manage contact details
- Record communications for a partner or contact
- Create a business partner
- Validation rules for partners and contacts

# Manage contact details

This article covers keeping a partner's contact people up to date — editing their details, changing the primary contact, and deactivating people who have left. These actions require the `crm:manage` permission.

## Edit a contact

Use editing to correct a name, update a job title, move someone to a new department, or add notes after a conversation.

1. Open the **CRM** module and go to the partner's detail page.
2. Open the contact you want to change and choose to edit it.
3. Update the salutation, first name, last name, position, department, or notes.
4. Adjust the **primary contact** and **active** flags as needed.
5. Save. The contact's updated timestamp is set and the change is written to the CRM audit trail.

The **first name** and **last name** remain required. Optional fields are trimmed on save, and clearing one stores it as empty.

## Change the primary contact

The primary contact appears first in the partner's contact list. To move the flag to a different person, edit the new main contact and turn on **primary contact**; edit the previous one and turn it off. Keeping a single primary per partner keeps the record unambiguous for everyone.

## Contact ordering

Contacts are always listed with the primary contact first, then alphabetically by last name and then first name. You do not set the order manually — it follows the primary flag and the names.

## Deactivate a contact

When someone leaves the partner organization or changes role, deactivate them rather than deleting. Deactivation is a soft delete that preserves the person's history and their communications.

1. Open the contact under the partner.
2. Choose to delete/deactivate the contact.
3. Confirm. The contact's **active** flag is set to false, an updated timestamp is recorded, and the action is logged in the CRM audit trail.

> **Note:** Deactivating a contact does not remove it. The record and its communications stay in the database. There is no hard-delete action for contacts.

## Reactivate a contact

To bring a contact back, open them, edit, turn the **active** flag back on, and save.

## How active status affects reporting

The CRM dashboard counts only *active* contacts. This drives several figures — total contacts, the count of partners that have at least one contact, and the count of partners that have a primary contact. Deactivating a contact therefore lowers these coverage numbers, which is intended: it reflects who you can actually reach today.

> **Tip:** If a partner shows no reachable people after a reorganization, add the new contact and mark them primary. The dashboard's "partners without a primary contact" figure helps you spot records that need attention.

## Managing a contact's communications

A contact's phone, mobile, and email entries are maintained separately from their profile details, on the contact's own communications log. See the related communications articles for how to add, reorder, and replace those entries.

## Related

- Add contacts to a partner
- Record communications for a partner or contact
- Update the communications log
- The CRM dashboard

# Create a business partner

This article shows you how to create a business partner (Geschäftspartner) in the CRM module — for example a leasing company, a workshop, or an insurer — so your fleet team shares one verified record for that organization.

## Before you start

Creating a partner requires the `crm:manage` permission, held by default by the SystemAdmin and FleetManager roles. If you can open the CRM module but the option to add a partner is missing, you likely have view-only access (`crm:view`).

## Choose the partner type first

Every partner must be classified as one of exactly two types. Pick the type by the role the organization plays for your fleet:

- **Supplier** — leasing companies, workshops, tyre and glass services, fuel-card vendors, and other suppliers.
- **Insurance** — insurers and insurance brokers.

> **Note:** The type is required and must be either *Supplier* or *Insurance*. A leasing company or workshop is a *Supplier*; only insurers use *Insurance*.

## Create the partner

1. Open the **CRM** module and go to the Partners list.
2. Start a new partner (the "new" partner form).
3. Select the **partner type** (Supplier or Insurance).
4. Enter the **name** — this is the only other required field.
5. Fill in the optional details you have (see the field reference below).
6. Save. The partner is created as **active**, with a creation timestamp, and the action is recorded in the CRM audit trail.

## Field reference

| Field | Required | Notes / limit |
| --- | --- | --- |
| Partner type | Yes | Supplier or Insurance |
| Name | Yes | Up to 200 characters |
| Short name | No | Up to 100 characters; handy for long legal names |
| Tax ID (Steuernummer) | No | Up to 100 characters |
| VAT number (USt-IdNr.) | No | Up to 100 characters |
| Policy-number prefix | No | Up to 50 characters; most relevant for Insurance partners |
| Street | No | Up to 200 characters |
| Postal code | No | Up to 20 characters |
| City | No | Up to 100 characters |
| Country | No | Up to 100 characters; used by the dashboard's geographic breakdown |
| Website | No | Up to 200 characters |
| Notes | No | Up to 2000 characters |

Blank optional fields are stored as empty, and leading or trailing spaces are trimmed automatically on save.

## Duplicate protection

> **Warning:** The system rejects a new partner if one already exists with the *same type and the same name*. If you see "A partner with the same type and name already exists," search the Partners list first — the organization may already be recorded, possibly as inactive.

## What to do next

After the partner is saved, open its detail page to add contact people and communications. For insurers, the record can also show linked insurance companies that are managed in the Versicherung module.

## Related

- Edit or deactivate a partner
- Add contacts to a partner
- Record communications for a partner or contact
- Validation rules for partners and contacts

# Edit or deactivate a partner

This article explains how to update a business partner's details and how to deactivate a partner you no longer work with, without losing its history. Both actions require the `crm:manage` permission.

## Edit a partner

Editing lets you correct addresses, add tax details, or reclassify a partner as your relationship changes.

1. Open the **CRM** module and find the partner in the Partners list.
2. Open the partner, then choose to edit it.
3. Change any of the fields — partner type, name, short name, tax ID, VAT number, policy-number prefix, address, website, or notes.
4. Save. The change sets an updated timestamp and is written to the CRM audit trail with your user identity.

As with creation, the **name** is required and the **partner type** must remain either Supplier or Insurance. Text fields are trimmed on save, and clearing an optional field stores it as empty.

> **Warning:** You cannot rename a partner to a name that another partner of the *same type* already uses. The update is rejected with "A partner with the same type and name already exists." Choose a distinct name or reconcile the duplicate.

## Activate or deactivate from the edit form

The edit form includes an **active** flag. Toggling it is the way to bring a partner back into use after it has been deactivated, or to mark one as no longer active while keeping all its data.

## Deactivate a partner

Deactivation is a soft delete: the partner is hidden from active lists but keeps its contacts, communications, and history so you can still refer to past dealings.

1. Open the partner you want to retire.
2. Choose to delete/deactivate it.
3. Confirm. The partner's **active** flag is set to false, an updated timestamp is recorded, and the action is logged in the CRM audit trail.

> **Note:** Deactivating a partner does *not* erase it. The record and everything under it remain in the database and can be viewed or reactivated later. There is no hard-delete action in the CRM module — this protects your audit history and any references from other modules.

## Finding and restoring deactivated partners

By default the Partners list can be filtered by active state. To locate a partner you previously deactivated, filter the list to show inactive partners (or search by name). Open it, edit it, set the **active** flag back on, and save to restore it to normal use.

## What deactivation affects

- The partner drops out of active-only views and the default working lists.
- Its contacts and communications are preserved exactly as they were.
- Dashboard counts distinguish active from inactive partners, so deactivated records still appear in totals but are counted as inactive.

> **Tip:** Prefer deactivation over trying to remove a partner. If a leasing contract ends or you stop using a workshop, deactivate the partner so the relationship history stays intact and reporting remains accurate.

## Related

- Create a business partner
- Search partners and contacts
- Manage contact details
- Validation rules for partners and contacts

# Search partners and contacts

This article shows you how to find business partners and their contacts in the CRM module, filter the partner list, and export the results. Browsing and searching require the `crm:view` permission.

## Search the partners list

Open the **CRM** module and go to the Partners list. The list supports a text search that matches across three fields:

- Partner **name**
- Partner **short name**
- **City**

Type part of any of these to narrow the list — for example a city name to see every partner there, or part of a company name. Results are always sorted alphabetically by partner name.

> **Note:** The search looks only at name, short name, and city. It does not search tax IDs, notes, contact names, or phone numbers, so search by the organization's name or location.

## Filter by type and status

You can combine the text search with two filters:

| Filter | Effect |
| --- | --- |
| Partner type | Show only Suppliers or only Insurance partners |
| Active status | Show only active or only inactive partners |

The module also offers quick-filtered **Suppliers** and **Insurances** views that pre-apply the type filter, so you land straight on the group you want.

## What the list shows

Each row summarizes a partner with its type, name, short name, city, country, a headline phone number, a headline email, and its active status. The headline phone is the partner's primary phone or mobile (falling back to the first available), and the headline email is the primary email — so keeping your primary communications flagged keeps this list accurate.

## Find a contact

Contacts are searched through their partner. Once you open a partner's detail page, its contacts are listed with the primary contact first, then alphabetically by last name and first name. To find a person:

1. Search or filter the Partners list to locate the organization.
2. Open the partner's detail page.
3. Scan the contact list — the primary contact sits at the top.

## Find deactivated records

Deactivated partners are hidden from active-only views but are never removed. Set the active-status filter to inactive to list them, then open one to review or reactivate it.

## Export the partner list

You can export the current partner list to CSV, which respects the same type, search, and active-status filters you have applied. The file includes each partner's ID, type, name, short name, city, country, primary phone, primary email, and active status.

> **Note:** Exporting requires the `crm:manage` permission, not just view access. Use it to share a partner directory or feed contact data into another system.

> **Tip:** To pull a clean list of, say, all active insurers, filter by type Insurance and active status, confirm the on-screen results, then export — the CSV matches exactly what you see.

## Related

- Create a business partner
- Edit or deactivate a partner
- The CRM dashboard
- Add contacts to a partner

# Validation rules for partners and contacts

This article lists the validation rules the CRM module enforces when you save partners, contacts, and communications, along with the exact messages you may see and how to resolve them. Creating or changing these records requires the `crm:manage` permission.

## Partner rules

| Rule | Message when broken |
| --- | --- |
| Name is required | "Name is required." |
| Partner type must be Supplier or Insurance | "PartnerType must be Supplier or Insurance." |
| Type + name must be unique | "A partner with the same type and name already exists." |

These apply when both creating and updating a partner. The name and type are the only mandatory fields; all address, tax, and other details are optional. Text is trimmed on save, so leading and trailing spaces do not create false differences.

> **Note:** Uniqueness is checked on the combination of type and name. You can have a Supplier and an Insurance partner with the same name, but not two Suppliers with the same name. The check ignores partners of a different type and, on update, ignores the partner you are editing.

## Field length limits for partners

- Name up to 200 characters; short name, tax ID, VAT number, city, country, and website up to 100–200 characters each.
- Policy-number prefix up to 50, postal code up to 20, and notes up to 2000 characters.

## Contact rules

| Rule | Message when broken |
| --- | --- |
| First name is required | "FirstName is required." |
| Last name is required | "LastName is required." |

Contacts have no uniqueness rule, so two people with the same name are allowed. Salutation, position, and department are limited to 100 characters (salutation 50), and notes to 1000. A contact must belong to an existing partner — adding one to a partner that cannot be found returns "Partner not found."

## Communication rules

| Rule | Message when broken |
| --- | --- |
| Each entry needs a value | "Communication value is required." |
| Type must be Phone, Mobile, Fax, Email, Website, or Other | "CommunicationType is invalid." |
| At most one primary entry per type | "Only one primary communication is allowed for type …" |

A communication value can be up to 256 characters and an optional label up to 100. Saving a communications log replaces the whole list, so include every entry you want to keep. If a sort-order value is left at zero or below, the system assigns one automatically by list position.

## How errors are returned

When a save fails validation, all applicable messages are combined and returned together, so you can see and fix everything at once rather than one error at a time. Records are saved only when every rule passes.

> **Tip:** If a save is rejected as a duplicate partner, search the Partners list — including inactive records — before creating a new one. The organization may already exist and simply need reactivating.

## Related

- Create a business partner
- Add contacts to a partner
- Update the communications log
- Edit or deactivate a partner

# What the CRM module does for your fleet

The CRM module in MobilityManager is where you keep a clean, shared record of the external organizations your fleet works with — and the people you deal with at each one. This article explains what the module covers and how it fits into day-to-day fleet operations.

## What the CRM module is for

A fleet rarely runs on its own. You order cars from leasing companies, send vehicles to workshops for repairs, and file claims with insurers. The CRM module gives every one of these relationships a single home: a business partner (Geschäftspartner) record with addresses, tax details, contact people (Ansprechpartner), and a full communications log. Instead of tracking phone numbers in spreadsheets or inboxes, your team looks up the partner once and sees everything.

## What you can store

- **Business partners** — one record per organization, classified as a *Supplier* or an *Insurance* partner.
- **Contacts** — the individual people at that partner, with salutation, position, and department.
- **Communications** — phone, mobile, fax, email, website, and other entries, held both at the partner level and per contact.
- **Addresses and identifiers** — street, postal code, city, country, tax ID, VAT number, and (for insurers) a policy-number prefix.

## Partner types in a fleet context

The module uses exactly two partner types, so classify each organization by the role it plays for your fleet:

| Type | Use it for |
| --- | --- |
| Supplier | Leasing companies, workshops, tyre services, fuel-card providers, and any vendor you buy services from. |
| Insurance | Insurers and insurance brokers you hold policies or file claims with. |

> **Note:** A leasing company or a workshop is stored as a *Supplier*. Only insurers use the *Insurance* type, which unlocks the policy-number prefix field and a link to insurance companies managed in the Versicherung (insurance) module.

## Where the module lives and who can use it

You reach the module from the admin area under the **CRM** section, which contains a Dashboard, a Partners list, and quick-filtered Suppliers and Insurances views.

- **View access** requires the `crm:view` permission. With it you can browse partners, contacts, and communications.
- **Editing** — creating, updating, or deactivating partners and contacts, and changing communications — requires the `crm:manage` permission.

By default, SystemAdmin and FleetManager roles hold both permissions. The Driver role has no CRM access. Custom roles can be granted either permission individually. The module also has to be entitled for your tenant before it appears.

## Auditing and multi-tenancy

Every partner and contact you create, update, or deactivate, and every change to a communications log, is written to the CRM audit trail with the acting user and a trace ID. All CRM data is scoped to your tenant, so one organization's partners are never visible to another.

> **Tip:** Start by adding your most-used leasing company, primary workshop, and main insurer. Once those partners exist, other modules and your team can reference consistent, verified contact details straight away.

## Related

- Create a business partner
- Add contacts to a partner
- The CRM dashboard
- Search partners and contacts

# Assign a damage case to a vehicle and driver

This article explains how a damage case (Schadenfall) is linked to a vehicle and, optionally, to a driver — and the rules that govern changing those links later.

## Vehicle is required, driver is optional

Every damage case must be attached to a vehicle. The vehicle is set when you create the case and cannot be left blank. The driver is optional: you can name the person who was using the vehicle at creation, or leave it empty and add them later.

Both the vehicle and, if supplied, the driver must already exist in your fleet. If you reference one that does not exist, the save is rejected with a "Vehicle not found" or "Driver not found" message.

> **Note:** Linking the case to a vehicle is what makes it appear in that vehicle's history, so choose the correct vehicle carefully at creation.

## Assigning or changing the driver

You can add, change, or clear the driver on an open or in-repair case. This is useful when the driver is identified only after the damage is first reported.

1. Open the case in the detail view.
2. Set the driver to the correct person, or clear it to leave the case with no driver.
3. Save. The driver link is updated.

Clearing the driver removes the link entirely, returning the case to an unassigned-driver state. Assigning a driver requires the **damage-management:transition** permission.

## Changing the vehicle

The vehicle can only be changed while the case status is **open**. This guards against a case being reattached to a different vehicle after work or repair has already begun.

| Case status | Change vehicle? | Change driver? |
| --- | --- | --- |
| open | Yes | Yes |
| in\_repair | No | Yes |
| closed | No (case is read-only) | No (case is read-only) |

> **Warning:** If you attached the wrong vehicle and the case is already in repair, you cannot simply swap the vehicle. Move the case back to open first (if the workflow allows) or create a corrected case, because the vehicle is locked once the case leaves the open state.

## Who can do this

Assigning or changing the vehicle and driver requires the **damage-management:transition** permission. SystemAdmin and FleetManager hold this by default. A Driver does not, and cannot reassign cases unless a custom role grants the permission.

## Where the links appear

Once assigned, the case list and the detail view show the vehicle's licence plate and the driver's name, so you can identify the affected vehicle and person at a glance without opening each record.

## Related

- Create a damage case (Schadenfall)
- Read a damage case in the detail view
- Find damage cases with the list and filters
- Move a damage case through the workflow

# Close a damage case

This article explains how to close a damage case (Schadenfall), the data the system requires before it will let you close, and why closing is final.

## Closing is a status transition

Closing a case simply means transitioning its status to **closed**. You can close a case from either the **open** or the **in\_repair** status. Once closed, the case is terminal — there is no way to reopen it.

Closing requires the **damage-management:transition** permission, held by SystemAdmin and FleetManager by default.

## Closure prerequisites

To prevent empty cases being closed with no financial trace, the system requires **one of two things** before it accepts a closure:

- The case has at least one **financial line item** (recording the repair costs), or
- You supply a **no-repair reason** explaining why there are no costs.

If a case has neither, the closure is refused with a "closure prerequisites not met" message and the status stays as it was.

> **Note:** The no-repair reason is stored on the case. Use it to record why the case closed without repair costs — for example "cosmetic scratch, no repair required" or "damage waived by third party".

## Close a case with repair costs

1. Open the case in the detail view.
2. Make sure the repair costs are captured as financial line items.
3. Transition the case to **closed**. Because line items exist, no reason is needed.

## Close a case without repair

1. Open the case in the detail view.
2. Transition the case to **closed** and enter a no-repair reason when prompted.
3. Confirm. The reason is saved with the case.

## What closing locks

Closing makes the case read-only. After a case is closed you can no longer:

- edit its core data (Schadensnummer, dates, mileage, description, vehicle, or driver);
- change its fault classification or the Vorfindeschaden flag;
- add, edit, or delete financial line items.

> **Warning:** Because a closed case cannot be reopened or edited, complete everything first — classification, driver, insurer, and financial line items — and only then close. If you close too early, you will have to record any correction elsewhere.

## Audit trail

The move to closed is written to the case's status history with a timestamp and the user who performed it, so the closure is always traceable.

## Related

- Move a damage case through the workflow
- Damage-case workflow: statuses and allowed transitions
- Who can change a case's status (permissions)
- Classify fault and mark pre-existing damage

# Create a damage case (Schadenfall)

This article shows you how to open a new damage case (Schadenfall) in the Damage Management module and which details you must supply before the case can be saved.

## Before you start

Creating a case requires the **damage-management:transition** permission. This is held by SystemAdmin and FleetManager by default; a Driver cannot create cases unless a custom role grants the permission. The vehicle you want to attach must already exist in the fleet, and if you plan to name a driver, that driver record must exist too.

> **Note:** The Schadensnummer (damage number) is entered by hand and must be unique within your tenant. If the number is already in use you will get a "Schadensnummer already exists" conflict and the case will not be saved.

## Open the new-case form

1. Go to the Damage Management module and open the case list.
2. Start a new case (the "New case" action). The create form opens.
3. Fill in the required fields listed below, then save.

## Required fields

All of these must be provided or the form will report the missing fields and refuse to save.

| Field | Meaning |
| --- | --- |
| Schadensnummer | Tenant-unique damage number. Trimmed and checked for duplicates on save. |
| Mileage | Vehicle mileage (km) at the time of the damage. |
| Damage date (Schadensdatum) | The date the damage occurred. |
| Notification date (Meldedatum) | The date the damage was reported to you. |
| Description | Free-text account of the damage (up to 2000 characters). Use this for the circumstances and location. |
| Vehicle | The affected vehicle. Required, and must already exist in the fleet. |

## Optional field

- **Driver** — You may name the driver who was using the vehicle, but this is optional. You can leave it blank and assign a driver later while the case is still open.

## What happens on save

When the case is created it is given the status **open** automatically and a creation timestamp is recorded. You are returned the new case, which you can then open in the detail view to add classification, an insurer, financial line items, deadlines, and documents.

> **Tip:** There is no separate "incident location" field. Record where the damage happened, and any other circumstances, inside the Description so the information travels with the case.

## Common errors

- **Duplicate Schadensnummer** — the number is already used by another case in your tenant. Choose a different number.
- **Vehicle not found** — the selected vehicle does not exist. Add or correct the vehicle first.
- **Driver not found** — a driver was named but no matching record exists. Leave the driver blank or pick a valid one.

## Related

- Record the incident date, report date, and mileage
- Assign a damage case to a vehicle and driver
- Classify fault and mark pre-existing damage
- Move a damage case through the workflow

# Find damage cases with the list and filters

This article shows you how to browse the damage-case (Schadenfall) list, apply filters, and page through results to find the case you need.

## Opening the list

The case list lives in the Damage Management module. It shows one row per case, newest first (ordered by creation date). Viewing the list requires the **damage-management:view** permission, held by SystemAdmin and FleetManager by default.

## What each row shows

Each case in the list surfaces its key identifiers so you can recognise it without opening it:

- Schadensnummer (damage number) and current status
- Fault type and the pre-existing-damage (Vorfindeschaden) flag
- Vehicle licence plate and driver name
- Insurer name (if one is assigned)
- Damage date, notification date, and creation date

## Available filters

You can narrow the list with any combination of the following filters. They are applied together, so adding more filters returns fewer, more specific results.

| Filter | Effect |
| --- | --- |
| Status | Show only cases in a given status: open, in\_repair, or closed. |
| Fault type | Show only cases with a given fault classification (Eigenverschulden or Fremdverschulden). |
| Vehicle | Show only cases for one vehicle. |
| Driver | Show only cases assigned to one driver. |
| Insurer | Show only cases assigned to one insurance company. |
| Date from / date to | Show cases whose damage date or notification date falls in the range. |

> **Note:** The date range matches a case if either its damage date or its notification date is inside the range. A case with neither date set falls back to its creation date. The "date to" boundary includes the whole final day.

## Paging

Results are paged. You can choose a page and a page size; the page size can be up to 100 cases per page. Each response also tells you the total number of matching cases, so you know how many pages to expect.

> **Tip:** To build a work queue of cases still needing attention, filter by status **open** or **in\_repair**. To review a single vehicle's damage history, filter by that vehicle.

## From the list to a case

Select any row to open the full case in the detail view, where you can edit core data, set classification, manage financial line items, and move the case through the workflow (subject to your permissions).

## Related

- Read a damage case in the detail view
- Create a damage case (Schadenfall)
- Classify fault and mark pre-existing damage
- Record the incident date, report date, and mileage

# Move a damage case through the workflow

This article shows you how to change a damage case's (Schadenfall) status — for example moving it into repair or back to open — and what the system checks before it accepts the change.

## What you need

Changing status requires the **damage-management:transition** permission. SystemAdmin and FleetManager have it by default; a Driver does not. Without the permission the transition is refused as forbidden.

## Perform a transition

1. Open the case in the detail view.
2. Choose the target status — for example **in\_repair** or back to **open**.
3. Confirm. If the move is allowed, the status updates immediately and the change is recorded in the case history.

> **Tip:** You do not have to pass through in\_repair. If a case needs no repair you can move it directly from open to closed. See "Close a damage case" for the extra data closing requires.

## Allowed moves at a glance

| Current status | Available moves |
| --- | --- |
| open | Mark in repair, or close |
| in\_repair | Return to open, or close |
| closed | None — the case is final |

## Typical sequences

### Standard repair

open → in\_repair → closed. Record the case, mark it in repair when the workshop takes the vehicle, then close it once the repair and its costs are captured.

### No repair needed

open → closed. Close the case directly, supplying a no-repair reason so the closure is justified.

### Correcting a premature move

in\_repair → open. If a case was marked in repair by mistake, move it back to open, which also unlocks the vehicle field again.

## Why a transition might be rejected

- **Invalid transition** — the move is not on the allowed list (for example trying to change a case that is already closed). Closed cases are terminal.
- **Forbidden** — you lack the damage-management:transition permission.
- **Closure prerequisites not met** — you are closing a case that has neither financial line items nor a no-repair reason.
- **Not found** — the case ID does not exist in your tenant.

> **Note:** Every accepted status change is written to the case's status history with the new status, a timestamp, and the user who made it, so you always have an audit trail of how the case moved.

## Related

- Damage-case workflow: statuses and allowed transitions
- Close a damage case
- Who can change a case's status (permissions)
- Assign a damage case to a vehicle and driver

# Read a damage case in the detail view

This article walks you through the damage-case (Schadenfall) detail view — what it shows, what you can do from it, and how permissions shape the actions available.

## Opening a case

Select any case from the list, or open it directly by its ID, to reach the detail view. Reading a case requires the **damage-management:view** permission. If the case does not exist in your tenant you will see a "not found" message.

## What the detail view shows

The detail view brings the case's core identity together in one place:

| Field | Description |
| --- | --- |
| Schadensnummer | The tenant-unique damage number. |
| Status | Current lifecycle status: open, in\_repair, or closed. |
| Fault type & Vorfindeschaden | The fault classification and whether the damage pre-existed. |
| Vehicle | The affected vehicle, shown with its licence plate. |
| Driver | The assigned driver's name, if one is set. |
| Insurer | The assigned insurance company, if one is set. |
| Damage & notification dates | When the damage occurred and when it was reported. |
| Created | When the case was recorded. |

## Actions from the detail view

With the **damage-management:transition** permission you can manage the whole case from here:

- Edit the core data (dates, mileage, description, vehicle while open, driver).
- Set the fault classification and the Vorfindeschaden flag.
- Assign an insurer and manage claims.
- Add and edit financial line items.
- Set deadlines with reminders.
- Attach documents.
- Move the case through the workflow, including closing it.

> **Note:** Users with only **damage-management:view** can read everything in the detail view but cannot edit the case or change its status.

## How status changes the view

The detail view respects the case's status. While a case is open you can change its vehicle; once it moves to in\_repair the vehicle is locked. Once the case is **closed** the whole case becomes read-only — core data, classification, and financial line items can no longer be changed.

> **Tip:** The status history behind a case records each transition with a timestamp and the user who made it, giving you a complete audit trail of how the case reached its current state.

## Related sub-records

A case is the hub for several related records — financial line items, insurance claims and their communications, deadlines, and documents. Each has its own view accessed from the case, so the detail view is your starting point for the full history of a damage.

## Related

- Find damage cases with the list and filters
- Move a damage case through the workflow
- Classify fault and mark pre-existing damage
- Close a damage case

# Record the incident date, report date, and mileage

This article explains how the incident date, notification date, mileage, and location details are captured on a damage case (Schadenfall), and how those values are later used for filtering and reporting.

## The two dates on a case

Every damage case records two distinct dates. Keeping them separate matters because reminders, filters, and reports treat them differently.

| Field | What it means |
| --- | --- |
| Damage date (Schadensdatum) | The date the damage actually occurred. |
| Notification date (Meldedatum) | The date the damage was reported to your team. |

Both dates are mandatory when you create a case. They can differ — a driver may report a scratch several days after it happened — and you should enter what is true for each rather than copying one into the other.

## Mileage at the time of damage

The **mileage** field records the vehicle's odometer reading (in km) at the moment of the damage. It is required at creation and is stored as a whole number. Use the reading closest to the incident so cost-per-kilometre and vehicle-history views stay accurate.

## Where the location goes

There is no dedicated location field on a damage case. Record where the incident happened — car park, motorway junction, depot yard — together with the circumstances in the **Description** field, which holds up to 2000 characters.

> **Tip:** Write the Description in a consistent shape, for example "Location — what happened — parties involved". Because the field travels with the case into the detail view and exports, a predictable format saves time later.

## Editing dates and mileage after creation

You can correct the damage date, notification date, mileage, and description at any time while the case is still open or in repair. Editing requires the **damage-management:transition** permission (SystemAdmin and FleetManager by default).

1. Open the case in the detail view.
2. Edit the core-data fields you need to change.
3. Save. Only the fields you changed are updated, and an "updated" timestamp is recorded.

> **Warning:** Once a case is **closed** it becomes read-only. You cannot change its dates, mileage, or description. Fix any incorrect dates before you close the case.

## How dates drive filtering

When you filter the case list by a date range, a case matches if its damage date or its notification date falls in the range. If a case somehow has neither date, its creation date is used as a fallback. This is why entering both dates correctly makes cases far easier to find later.

## Related

- Create a damage case (Schadenfall)
- Find damage cases with the list and filters
- Read a damage case in the detail view
- Assign a damage case to a vehicle and driver

# Who can change a case's status (permissions)

This article explains which roles and permissions are needed to change a damage case's (Schadenfall) status, and how permissions differ from simply viewing cases.

## Two permissions govern the module

Damage Management is controlled by two permissions. Understanding the split explains who can do what.

| Permission | Allows |
| --- | --- |
| **damage-management:view** | See the case list, open the detail view, and read financial data, deadlines, claims, and analytics. |
| **damage-management:transition** | Create and edit cases, change status, set classification, assign an insurer, and manage financial line items and deadlines. |

Changing a case's status specifically requires **damage-management:transition**. Read-only users with only the view permission can watch a case progress but cannot move it.

## Which roles have it

| Role | view | transition |
| --- | --- | --- |
| SystemAdmin | Yes | Yes |
| FleetManager | Yes | Yes |
| Driver | No | No |

SystemAdmin automatically holds every permission in the system. FleetManager is the default operational role and can run the damage workflow end to end. The Driver role is self-service and includes no damage-management permissions at all, so drivers cannot see or transition cases by default.

> **Note:** The transition permission is checked twice for status changes — once when the request reaches the module, and again inside the workflow itself before the state machine runs. A user missing the permission is rejected as forbidden even if they reach the action.

## Granting access with custom roles

If someone needs to move cases but should not be a full FleetManager, a SystemAdmin can create a custom role that includes **damage-management:transition** (and usually **damage-management:view** so they can open cases). Assign that role to the user.

1. As a SystemAdmin, create or edit a custom role.
2. Add the damage-management view and transition permissions.
3. Assign the role to the users who need to work cases.

> **Tip:** Grant **damage-management:view** on its own to auditors or finance staff who need to read cases and reports without the ability to change status or edit data.

## What "forbidden" looks like

If you attempt to change a status without the transition permission, the action fails with a forbidden result and the status is left unchanged. Ask a SystemAdmin to grant you the permission or to make the change on your behalf.

## Related

- Move a damage case through the workflow
- Damage-case workflow: statuses and allowed transitions
- Close a damage case
- Find damage cases with the list and filters

# Damage-case workflow: statuses and allowed transitions

This article is a reference for the damage-case (Schadenfall) lifecycle: the three statuses a case can hold and exactly which status changes are allowed.

## The three statuses

A damage case is always in one of three canonical statuses. New cases start as **open**.

| Status | Meaning |
| --- | --- |
| **open** | The case has been recorded and is being worked. Core data (including the vehicle) can still be changed. |
| **in\_repair** | The vehicle is undergoing repair. The case is active but the vehicle can no longer be changed. |
| **closed** | The case is finished. It is read-only and cannot be reopened. |

## Allowed transitions

The workflow is enforced by a state machine, so only the moves below are possible. Any other move is rejected as an invalid transition.

| From | You can move to |
| --- | --- |
| open | in\_repair, closed |
| in\_repair | open, closed |
| closed | — (nothing; closed is final) |

A few consequences follow from this table:

- You can send a case straight from **open** to **closed** without ever marking it in repair — useful when no repair is needed.
- You can move a case back from **in\_repair** to **open**, for example if a repair was queued in error.
- **closed** is terminal. There is no transition out of it, so a closed case can never be reopened or moved again.

> **Warning:** Because closing is irreversible, treat the move to **closed** as final. Confirm the classification, driver, and financial data are correct before you close.

## What the status controls

The status does more than label a case — it gates what you can edit:

- The **vehicle** can only be changed while the case is open.
- A **closed** case is fully read-only: core data, classification, and financial line items can no longer be changed.
- Closing has its own prerequisites (see the article on closing a case).

> **Note:** Status values are stored in lower-case canonical form — `open`, `in_repair`, `closed` — and every change is written to the case's status history so you keep a full audit trail.

## Who can change status

Performing any transition requires the **damage-management:transition** permission, held by SystemAdmin and FleetManager by default. Viewing the status only requires **damage-management:view**.

## Related

- Move a damage case through the workflow
- Close a damage case
- Who can change a case's status (permissions)
- Read a damage case in the detail view

# Classify fault and mark pre-existing damage

This article explains how to classify a damage case (Schadenfall) by fault and how to mark it as pre-existing damage (Vorfindeschaden). These two settings drive filtering, analytics, and how a case is handled with insurers.

## The two classification settings

A damage case carries two independent classification values. You can set either or both.

| Setting | Values | Meaning |
| --- | --- | --- |
| Fault classification | Eigenverschulden or Fremdverschulden | Whether the damage was self-inflicted (Eigenverschulden, own fault) or caused by a third party (Fremdverschulden, third-party fault). |
| Vorfindeschaden | On or off | Whether the damage already existed and was only found later (pre-existing damage), rather than being newly caused. |

> **Note:** Fault classification and the Vorfindeschaden flag are separate. A case can be third-party fault and also flagged as pre-existing; setting one does not change the other. Fault classification may also be left unset if it is not yet known.

## Set or change the classification

Classification is edited from the case, and you can provide either value on its own or both together.

1. Open the case in the detail view.
2. Choose the fault classification — Eigenverschulden or Fremdverschulden — and/or toggle the Vorfindeschaden flag.
3. Save. At least one of the two must be provided for the update to be accepted.

Setting classification requires the **damage-management:transition** permission, held by SystemAdmin and FleetManager by default. Changes are audit-logged.

> **Tip:** Only Eigenverschulden and Fremdverschulden are accepted as fault values. Any other text is rejected as an invalid fault classification, so pick from these two.

## When you can classify

Classification can be set or changed at any time while the case is open or in repair. Once the case is **closed** the classification is frozen along with the rest of the case — a closed case will reject any classification change.

> **Warning:** Confirm the fault classification before you close the case. Because closing is final and read-only, an incorrect classification cannot be corrected on the case afterwards.

## Why it matters

- **Filtering** — you can filter the case list by fault type to isolate own-fault versus third-party cases.
- **Analytics** — the damage dashboard reports a fault distribution across your cases, so accurate classification keeps those figures meaningful.
- **Handling** — third-party (Fremdverschulden) cases are typically the ones you pursue with an insurer, while the Vorfindeschaden flag distinguishes damage that predates the current driver or usage period.

## Related

- Create a damage case (Schadenfall)
- Find damage cases with the list and filters
- Read a damage case in the detail view
- Close a damage case

# Damage analytics, the financial report, and data export

The Damage Management module offers three ways to see your damage data in aggregate: an analytics dashboard for operational KPIs, a financial report for money breakdowns, and a data export that produces a machine-readable payload for your ERP. This article explains what each one gives you and how they differ.

## Before you start

The dashboard, financial report, and export all require the **View damage management** permission (`damage-management:view`), held by SystemAdmin and FleetManager by default. All three are scoped to your tenant.

## The analytics dashboard

The dashboard summarizes your case portfolio and its costs. You can filter it by date range, status, vehicle, driver, and fault type; every panel honors the filters. It returns, among others:

- **Case volume**, plus counts of **open** and **in-repair** cases.
- **Total costs** and **average cost per case**, with net, VAT, and gross totals and average gross per case.
- **Status distribution** and **fault classification distribution**.
- **Insurer metrics**: claim count, settled count, and settlement rate per insurer.
- **Top vehicles** by number of cases (up to ten), with license plate.
- **Overdue deadlines count**.
- **Monthly trend** over the last 12 months and a **cost breakdown by cost center**.

> **Note:** On the dashboard, "total costs" and the monthly trend are based on *net* amounts, while the gross totals are shown separately. Keep this in mind when reconciling figures against the financial report.

## The financial report

The financial report focuses on money and breaks it down for accounting. It supports the same filters as the dashboard plus a **cost center** filter. It returns:

- A **summary**: total net, total VAT, total gross, total Eigenanteil (own share / deductible), case count, and line-item count.
- Breakdowns **by cost center**, **by vehicle** (with license plate), **by month**, and **by fault type**, each with net and gross amounts.

Use the report when you need the Eigenanteil rollup or a cost-center view that the dashboard does not surface.

## Exporting damage data

The export produces a versioned ERP posting payload (contract version `v1`) for a date range. It selects damage cases created within the range and includes, for each case, its case number (Schadensnummer), status, currency, and totals (net and gross), along with every financial line item.

Each exported line carries description, quantity, unit price, net amount, VAT rate, VAT amount, gross amount, Eigenanteil, cost center code, and GL mapping code, so downstream systems can post the costs. The payload also includes metadata: the tenant, the export date, and a trace identifier.

> **Tip:** The export selects cases by their *creation* date within the range you request, whereas the dashboard and report filter by the *damage* date. Choose the date range accordingly when reconciling an export against a dashboard view.

## Which one to use

| Need | Use |
| --- | --- |
| Operational overview and KPIs | Analytics dashboard |
| Cost breakdowns and Eigenanteil totals | Financial report |
| A structured payload to post into an ERP | Data export |

## Related

- Recording financial line items on a damage case
- Bulk-updating financial line items
- Setting and tracking case deadlines and reminders

# Attaching documents to a damage case

You can attach documents such as photos, estimates, and correspondence to a damage case, either by uploading a file or by ingesting a document from a URL. Documents are handled through the shared Document module using the "DamageCase" association. This article explains both methods and how to list, preview, and remove documents.

## Before you start

Uploading, ingesting, and deleting documents require the **Manage documents** permission (`documents:manage`). Listing and previewing documents require the **View documents** permission (`documents:view`). Ingesting from a URL through the damage case additionally uses the **Transition damage cases** permission (`damage-management:transition`). These are held by SystemAdmin and FleetManager by default.

## Upload a file

Open the damage case and upload a document to it. Each upload requires a file and a document category.

| Field | Required | Purpose |
| --- | --- | --- |
| File | Yes | The document to attach. Must not be empty. |
| Category | Yes | Must match an active system document category. Categorizing keeps a case's files organized. |
| Beleg reference | No | An optional link to a related Beleg (receipt/expense document). |

The file name, content type, size, uploader email, and upload date are recorded automatically. An uploaded document is stored as version 1; if content is added later, the most recent version is what downloads.

> **Note:** The category must be one of your tenant's active system document categories. If the category is missing or does not match an active one, the upload is rejected.

## Ingest a document from a URL

Instead of uploading a file, you can point the case at a document URL and let the system fetch it in the background. Submit the URL on the case; the request is validated, queued, and accepted immediately, returning a job identifier and a "Queued" status. The document is retrieved by a background worker.

URLs are checked before any download is attempted, to protect against server-side request forgery:

- The URL must be an absolute `http` or `https` address.
- Private and local network targets are blocked, including localhost, loopback, link-local, and private ranges such as 10.x, 172.16-31.x, and 192.168.x.
- If your tenant configures an allowed-host list, only hosts on that list are permitted.

> **Warning:** A rejected URL is never downloaded. If ingestion is refused, check that the address is a public http or https URL and, if an allow-list is configured, that the host is included.

## List, preview, and delete

- **List:** the case's document list shows each attachment's file name, content type, size, category, and date, newest first.
- **Preview or download:** retrieving a document returns the content of its latest version.
- **Delete:** removing a document deletes it and its versions from the case.

> **Warning:** Documents cannot be deleted from a *closed* case. Remove any unwanted attachments before closing the case.

## Related

- Recording financial line items on a damage case
- Assigning an insurer and opening a claim
- Damage analytics, the financial report, and data export

# Bulk-updating financial line items

Bulk update lets you save a damage case's entire set of financial line items in one operation, creating, editing, removing, and reordering lines together. This article explains its full-replacement behavior and the rules that apply.

## Before you start

Bulk update requires the **Transition damage cases** permission (`damage-management:transition`), held by SystemAdmin and FleetManager by default. As with single line items, bulk update is **blocked on a closed case**, so finalize costs before closing.

## How full replacement works

A bulk update submits the complete list of line items that should exist on the case. The system compares your list against what is currently stored and reconciles them:

- A line **with an existing id** updates that line in place.
- A line **without an id** is created as a new line.
- Any stored line **not included** in your list is deleted.

> **Warning:** Because omission means deletion, always submit the full set of lines you want to keep. If you send only the lines you changed, every line you left out will be removed from the case.

## Limits and validation

The same rules that govern single line items apply to every line in the batch, and a few batch-level limits are added:

- A maximum of **20 line items** per case. A larger list is rejected.
- Each line must pass validation: quantity greater than zero, unit price zero or greater, VAT rate between 0 and 100 (entered as a percentage such as 19), and Eigenanteil (own share / deductible) between zero and the line's gross amount.
- Every line must use the same currency. Lines are pinned to your tenant's configured currency so damage costs stay consistent with Beleg (receipt/expense document) costs across the platform.
- Each line requires a description.

If any line fails validation, the whole batch is rejected and nothing is saved, so the case is never left in a half-updated state.

## What you get back

A successful bulk update returns the resulting **item count** together with the recalculated **total net amount** and **total gross amount** for the case, so you can immediately confirm the new totals.

## When to use bulk update

1. Load the case's current line items into the editor.
2. Add new lines, edit existing ones, and remove any that no longer apply.
3. Set the sort order on each line to control how they are listed.
4. Save once to apply all changes together.

> **Tip:** Use bulk update to reorder lines or to rework an estimate in a single save. For a quick one-off change to a single line, editing that line directly is simpler and avoids the risk of accidentally dropping other lines.

## Related

- Recording financial line items on a damage case
- Damage analytics, the financial report, and data export
- Setting and tracking case deadlines and reminders

# Recording financial line items on a damage case

Financial line items record the itemized costs of a damage case, such as parts, labor, and fees, with quantities, VAT, and your own share (Eigenanteil). This article shows how to add, edit, and delete a single line item and how the totals are calculated.

## Before you start

Creating, editing, and deleting line items require the **Transition damage cases** permission (`damage-management:transition`). Viewing them requires the **View damage management** permission (`damage-management:view`). Both are held by SystemAdmin and FleetManager by default.

> **Warning:** Financial line items cannot be added, changed, or deleted once the damage case is *closed*. Record and finalize costs before closing the case.

## Add a line item

Open the damage case and add a line item to its financial section. Only the description is mandatory.

| Field | Purpose |
| --- | --- |
| Description | What the cost is for. Required. |
| Quantity | Number of units. Must be greater than zero. |
| Unit price | Price per unit. Must be zero or greater. |
| VAT rate | The VAT percentage, entered as a whole number such as 19 for 19%. Must be between 0 and 100. |
| Eigenanteil (own share / deductible) | The portion your organization bears. Must be zero or greater and cannot exceed the line's gross amount. |
| Payer split | Optional free-text note describing how the cost is shared between parties. |
| Cost center | Optional accounting code used in the financial report and ERP export. |
| GL mapping | Optional general-ledger mapping code used in the export. |
| Sort order | Controls the order line items appear in the list. |

## How amounts are calculated

You enter the quantity, unit price, and VAT rate; the system computes the rest and stores them on the line:

- **Net amount** = quantity x unit price.
- **VAT amount** = net amount x (VAT rate / 100).
- **Gross amount** = net amount + VAT amount.

All values are rounded to two decimal places. The list view sums these across all lines and shows total net, total VAT, total gross, and total Eigenanteil. The headline financial total for a case is the all-in **gross** amount.

## Currency

Line items use your tenant's configured currency so that damage costs aggregate consistently with Beleg (receipt/expense document) costs in dashboards and vehicle totals. All line items on a single case must use the same currency; a line in a different currency is rejected.

## Edit or delete a line item

Editing a line is partial: change only the fields you need and the others keep their current values. After any change, the same validation rules apply (quantity greater than zero, unit price zero or greater, VAT 0-100, Eigenanteil within the gross), and the net, VAT, and gross amounts are recalculated automatically. Deleting a line removes it from the case and updates the totals. Both actions are blocked on a closed case.

> **Tip:** If you need to change several lines at once, or add and remove lines in a single save, use the bulk update instead of editing lines one by one.

## Related

- Bulk-updating financial line items
- Damage analytics, the financial report, and data export
- Assigning an insurer and opening a claim

# Setting and tracking case deadlines and reminders

Case deadlines let you track the key dates on a damage case, such as when the insurer must respond or when a repair is due, and flag them automatically once they pass. This article explains how to set deadlines and how overdue reminders work.

## Before you start

Setting a deadline requires the **Transition damage cases** permission (`damage-management:transition`). Viewing deadlines requires the **View damage management** permission (`damage-management:view`). Both are held by SystemAdmin and FleetManager by default.

## Deadline types

A deadline has a type and a due date. Three deadline types are supported:

| Type | Tracks |
| --- | --- |
| `insurance_response` | The date by which you expect the insurer to respond. |
| `repair_completion` | The date the repair should be finished. |
| `claim_settlement` | The date by which the claim should be settled. |

A due date with any other type is rejected.

## Set or update a deadline

Open the damage case and set a deadline by choosing its type and due date. Each case can hold at most one deadline of each type, so the type and case together are unique.

- Setting a type that is not yet on the case **creates** a new deadline.
- Setting a type that already exists **updates** its due date.

> **Note:** When you change the due date of an existing deadline, its reminder is reset. This means a deadline that had already been flagged can be flagged again against the new date.

## How overdue reminders work

A background reminder check runs automatically every hour across all active tenants. It finds deadlines whose due date has passed and that have not yet been reminded, and stamps each with a **reminder-sent** timestamp. That timestamp records that the overdue deadline has been picked up, and it prevents the same deadline from being flagged repeatedly.

Separately, every deadline you view carries an **overdue** indicator that is true whenever its due date is in the past, so a deadline shows as overdue in the list immediately, without waiting for the hourly check.

## Review deadlines

- **Per case:** a case's deadline list shows every deadline for that case, ordered by due date, with its type, due date, overdue indicator, reminder-sent timestamp, and the case number (Schadensnummer).
- **Across all cases:** an overdue deadlines view lists every past-due deadline across all cases. This feeds the dashboard so you can see everything that needs attention in one place.

> **Tip:** When an insurer commits to a response date during a call, record it right away as an `insurance_response` deadline and log the call as a claim communication. The deadline then surfaces in the overdue view if the insurer misses it.

## Related

- Assigning an insurer and opening a claim
- Logging claim communications
- Damage analytics, the financial report, and data export

# Assigning an insurer and opening a claim

Before you can open an insurance claim on a damage case (Schadensfall), the case must have an insurer assigned. This article covers assigning the insurer, opening the claim, and updating it as the claim progresses.

## Before you start

Assigning an insurer, opening a claim, and updating a claim all require the **Transition damage cases** permission (`damage-management:transition`). Viewing claims requires the **View damage management** permission (`damage-management:view`). Both are held by SystemAdmin and FleetManager by default. The insurer you want must already exist in your tenant's insurance companies list.

## Assign an insurer to the case

Open the damage case and assign an insurer from the insurance companies list. The system verifies that both the case and the chosen insurer exist, then records the insurer on the case.

1. Open the damage case you want to work on.
2. In the case's insurer area, select an insurer from the list.
3. Save. The case now shows the assigned insurer.

> **Note:** You can reassign a different insurer later, but do this before you open a claim, because a claim is permanently tied to the insurer that was assigned when it was created.

## Open a claim

With an insurer assigned, open a claim against the case. The only required field is the claim number.

| Field | Required | Purpose |
| --- | --- | --- |
| Claim number | Yes | The insurer's reference number for this claim. |
| Insurer | Yes | Must match the insurer already assigned to the case. |
| Status | No | Free-text status you use to track the claim's progress. |
| Claim date | No | The date the claim was filed with the insurer. |
| Settlement amount | No | The amount agreed once the claim is settled. |
| Settlement date | No | The date the claim was settled. |

> **Warning:** If the claim's insurer does not match the insurer assigned to the case, the claim is rejected with the message "Insurance company must match the case's assigned insurer." Assign or correct the case insurer first, then open the claim.

## Update a claim as it progresses

As the insurer responds, update the claim to reflect its current state. Updates are partial, so you only send the fields that changed; anything you leave out keeps its current value.

- Change the **status** as the claim moves through its lifecycle.
- Record the **settlement amount** and **settlement date** when the insurer settles.
- Adjust the **claim date** if it needs correcting.

## Review claims on a case

The claims list shows all claims, most recently created first, and can be filtered by damage case or by insurer. Each entry shows the claim number, status, claim date, settlement amount, and settlement date, giving you a quick view of where every claim stands.

> **Tip:** Once a claim exists, use claim communications to keep a dated timeline of every email, call, and letter exchanged with the insurer.

## Related

- Maintaining insurance companies
- Logging claim communications
- Recording financial line items on a damage case
- Setting and tracking case deadlines and reminders

# Logging claim communications

Claim communications give each insurance claim a dated timeline of every contact with the insurer, so anyone picking up the case can see what was said and when. This article explains how to add and review communication entries.

## Before you start

Adding a communication requires the **Transition damage cases** permission (`damage-management:transition`). Viewing the timeline requires the **View damage management** permission (`damage-management:view`). Both are held by SystemAdmin and FleetManager by default and can be granted to custom roles. A claim must already exist on the case before you can log communications against it.

## Add a communication entry

Open the claim and add a communication to its timeline. Each entry captures when the contact happened, what kind of contact it was, and a short summary.

| Field | Required | Purpose |
| --- | --- | --- |
| Communication date | Yes | When the contact took place. Entries are ordered by this date. |
| Type | No | The kind of contact, for example email, phone, or letter. Defaults to `email` if left blank. |
| Summary | No | A short free-text note describing what was communicated. |

The person who is signed in is recorded automatically as the actor on the entry, so you do not enter who logged it. Their user and email are stored with the communication for accountability.

> **Note:** The communication date is separate from the date you create the entry. You can back-date an entry to reflect when a call or letter actually happened, which keeps the timeline accurate even if you log it later.

## Review the claim timeline

The communications list for a claim shows every entry with the newest communication date first. Each row shows the date, the type, the summary, and the email of the actor who recorded it. Reading top to bottom gives you the most recent activity first; the full list reconstructs the back-and-forth with the insurer.

## Good practices

- Log a communication as soon as it happens so the timeline stays complete and correctly ordered.
- Use the type field consistently (for example, always "phone" for calls) so entries are easy to scan.
- Keep summaries short but specific: note decisions, amounts discussed, and any commitments or deadlines the insurer gave.
- When the insurer sets a deadline for a response, also record it as a case deadline so it is tracked and flagged if it becomes overdue.

> **Tip:** Pair the communication timeline with the claim's status field. Update the status to reflect the claim's current stage, and use communications to explain what drove each change.

## Related

- Assigning an insurer and opening a claim
- Maintaining insurance companies
- Setting and tracking case deadlines and reminders

# Maintaining insurance companies

Insurance companies (Versicherungen) are the tenant-wide directory of insurers you assign to damage cases and reference on claims. This article explains how to add an insurer and how the list behaves.

## Before you start

Adding an insurer requires the **Transition damage cases** permission (`damage-management:transition`). Viewing the insurer list requires the **View damage management** permission (`damage-management:view`). Both permissions are held by SystemAdmin and FleetManager by default and can be granted to custom roles. Insurers are stored per tenant, so each organization maintains its own list.

## Add an insurance company

In the Damage Management module, open the insurance companies area and create a new insurer. Only the name is mandatory; the other fields are optional and can be left blank.

| Field | Required | Purpose |
| --- | --- | --- |
| Name | Yes | The insurer's display name. Used everywhere the insurer is shown and picked. |
| Policy number prefix | No | A prefix that helps you recognize this insurer's policy numbers. |
| Address | No | Postal address of the insurer. |
| Contact details | No | Free-text notes such as a claims hotline or contact person. |

When you save, the new insurer is created and immediately marked active, so it appears in insurer pickers on damage cases right away.

> **Note:** Leading and trailing spaces are trimmed from every field, so "Allianz " and "Allianz" are treated as the same name.

## How insurers link to your CRM partners

Creating an insurer also keeps your CRM partner directory in sync. The system looks for an existing CRM partner of type Insurance with the same name:

- If none exists, a new insurance partner is created with the same name, policy number prefix, address, and contact notes.
- If a partner with that name already exists, it is reused. Any empty fields on that partner are back-filled from your input, but existing values are never overwritten.

This means you can maintain insurers from the Damage Management module without creating duplicate CRM records.

## View the insurer list

The insurance companies list shows every active insurer for your tenant, sorted alphabetically by name. Each row displays the name, policy number prefix, address, and active state. This list feeds the insurer picker when you assign an insurer to a case, so keeping it accurate keeps case assignment fast.

> **Tip:** Create the insurer once, centrally, before you start assigning it to cases. Because a claim's insurer must match the insurer assigned to its case, a clean, deduplicated list prevents mismatches later.

## Related

- Assigning an insurer and opening a claim
- Logging claim communications
- Recording financial line items on a damage case

# Configuring the document encryption-at-rest key

MobilityManager can encrypt uploaded documents at rest when they are stored on local storage. This is controlled by the `DocumentStorage` configuration section. This article explains the encryption key requirement and how to configure it safely.

## The DocumentStorage section

Two keys govern encryption at rest, both under the root `DocumentStorage` section:

- `DocumentStorage:EnableEncryptionAtRest` — turns encryption on or off. It defaults to on.
- `DocumentStorage:LocalEncryptionKey` — the key used to encrypt and decrypt local documents.

> 
> **Note:** If you previously configured these under `DamageManagement:DocumentStorage:*`, migrate to the root `DocumentStorage` keys. The old location is no longer used.

## Startup enforcement

Outside the Development environment, the application fails to start if encryption at rest is enabled but no key is configured. The startup check applies when `DocumentStorage:EnableEncryptionAtRest` is on (or left at its default) and `DocumentStorage:LocalEncryptionKey` is empty. The error instructs you to supply a base64-encoded 32-byte key or to disable encryption at rest.

> 
> **Warning:** The key is intentionally not committed to `appsettings.json`. A deployed instance with encryption enabled must supply the key through an environment variable or secret store.

## Generate a key

The key must be a base64-encoded 32-byte value:

    openssl rand -base64 32

Use the entire output as the value of `DocumentStorage__LocalEncryptionKey`.

## Provide the key by environment

- **Docker / Compose / published:** set the environment variable

        DocumentStorage__LocalEncryptionKey=base64-32-byte-key
        DocumentStorage__EnableEncryptionAtRest=true
- **Kubernetes:** keep `DocumentStorage__EnableEncryptionAtRest` in the ConfigMap and place `DocumentStorage__LocalEncryptionKey` in the Secret.

## If you do not need encryption

To run without encryption at rest — for example on evaluation systems — explicitly disable it:

    DocumentStorage__EnableEncryptionAtRest=false

With the feature disabled, no key is required and the startup check is skipped.

> 
> **Warning:** Treat the encryption key like any other secret and back it up securely. If you lose it, previously encrypted documents cannot be decrypted. Rotating the key requires re-encrypting existing content.

## Related

- Setting the JWT signing key for production
- Configuring environment variables and the .env file
- Production hardening checklist
- Troubleshooting a failed startup

# Configuring environment variables and the .env file

MobilityManager is configured through standard .NET configuration, which reads from environment variables. This article lists the settings you will most often provide and explains how the `.env` file feeds them into the Docker Compose stacks.

## How environment variables map to settings

Nested configuration keys use a double-underscore (`__`) separator in environment-variable form. For example, `Jwt:Key` becomes `Jwt__Key`, and `ConnectionStrings:SystemConnection` becomes `ConnectionStrings__SystemConnection`.

## Core settings

| Variable | Description | Required | Default |
| --- | --- | --- | --- |
| `DatabaseProvider` | `PostgreSQL` or `SqlServer` | Yes | — |
| `ConnectionStrings__SystemConnection` | System database connection string | Yes | — |
| `ConnectionStrings__TenantConnection` | Tenant database connection string | Yes | — |
| `Jwt__Key` | JWT signing key (min 32 bytes) | Yes | — |
| `Jwt__Issuer` | JWT issuer identifier | No | `CK.MobilityManager` |
| `Jwt__Audience` | JWT audience identifier | No | `CK.MobilityManager.Users` |
| `DocumentStorage__EnableEncryptionAtRest` | Toggle document encryption at rest | No | `true` |
| `DocumentStorage__LocalEncryptionKey` | Base64 32-byte key, required when encryption is on | Conditional | — |
| `License__PublicKey` | RSA public key for license validation | No | — |
| `ASPNETCORE_ENVIRONMENT` | Environment name | No | `Production` |
| `ASPNETCORE_URLS` | Kestrel binding inside the container | No | `http://+:8080` |

## The .env file for Docker Compose

Copy `.env.example` to `.env` and set your own values. The Compose files read variables such as `POSTGRES_PASSWORD`, `JWT_KEY`, `DOCKER_REGISTRY`, `DOCKER_IMAGE`, and `VERSION` and inject the resulting configuration into the containers:

    POSTGRES_PASSWORD=YourStrongPassword123!
    JWT_KEY=YourSecretKeyThatMustBeAtLeast32CharactersLong!
    JWT_ISSUER=CK.MobilityManager
    JWT_AUDIENCE=CK.MobilityManager.Users
    DOCKER_REGISTRY=ghcr.io
    DOCKER_IMAGE=your-org/ck-mobility-manager
    VERSION=latest

> 
> **Warning:** The values in `.env.example` are placeholders. The example `JWT_KEY` is a known committed default and is rejected outside Development — always generate your own.

## Where each environment reads configuration

- **Docker Compose:** the `.env` file plus values hard-coded in the Compose service definitions.
- **Kubernetes:** the `mobilitymanager-config` ConfigMap and `mobilitymanager-secrets` Secret.
- **Published deployment:** environment variables from your process manager, plus `appsettings.json`.

> 
> **Tip:** Keep secrets (database passwords, `Jwt__Key`, the encryption key) out of ConfigMaps and source control. Use a secret store and inject them at runtime.

## Related

- Configuring the database connection strings
- Setting the JWT signing key for production
- Configuring the document encryption-at-rest key
- Production hardening checklist

# Production hardening checklist

Use this checklist to review a MobilityManager deployment before it goes live. Each item maps to a setting or behaviour enforced by the platform. Work through them alongside the linked detailed articles.

## Secrets and keys

- Set a strong `Jwt__Key` of at least 32 bytes that is not a committed default. The app refuses to start in Production otherwise.
- Set `DocumentStorage__LocalEncryptionKey` (base64 32-byte) whenever encryption at rest is enabled, or explicitly disable it.
- Use a unique, strong database password and never reuse example values from `.env.example`.
- Store all secrets in a secret store or Kubernetes Secret — never in ConfigMaps or source control.

## Environment and database

- Run with `ASPNETCORE_ENVIRONMENT=Production` so fail-fast security checks are active.
- Set `DatabaseProvider` to `PostgreSQL` or `SqlServer` and point `SystemConnection` and `TenantConnection` at managed databases.
- For SQL Server connections, keep `Encrypt=True` and validate the server certificate.
- Confirm the account in each connection string has only the privileges it needs.

## Transport and network

- Terminate TLS at a reverse proxy, load balancer, or ingress; the container serves plain HTTP on port 8080.
- Do not expose the container port directly to the internet.
- Restrict database ports to the application network only.

> 
> **Warning:** The development Docker Compose file publishes PostgreSQL on port 5432 for convenience. Do not carry that port mapping into a production host.

## Runtime and container hardening

- Run the container as a non-root user with a read-only root filesystem and dropped Linux capabilities, as the Kubernetes base manifests do.
- Provide writable volumes only where needed (`/tmp`, `/app/data`, `/app/logs`).
- Set resource requests and limits so a single instance cannot exhaust node memory.
- Pin the image to a specific version tag rather than `latest`.

## Availability and monitoring

- Run at least two replicas with a PodDisruptionBudget so upgrades and node drains keep the service available.
- Wire liveness, readiness, and startup probes to `/livez`, `/readyz`, and `/healthz`.
- Monitor the health endpoints from your NOC or uptime tooling.

## Data protection

- Configure automated backups for both the system and tenant databases and test a restore.
- Back up the JWT and document encryption keys securely; losing the encryption key makes encrypted documents unrecoverable.

> 
> **Note:** Uploads are capped at 50 MB per request by the application. Keep this in mind when sizing reverse-proxy body limits so large but valid uploads are not blocked upstream.

## Related

- Setting the JWT signing key for production
- Configuring the document encryption-at-rest key
- Kubernetes deployment overview
- Backing up and restoring your databases

# Setting the JWT signing key for production

MobilityManager signs authentication tokens with a symmetric JWT key. This key is security-critical: anyone who knows it can forge tokens, including SystemAdmin tokens. This article explains the requirements and how to set the key safely in production.

## Requirements enforced at startup

Outside the Development environment, the application refuses to start unless `Jwt:Key` meets all of these conditions:

- It is present and non-empty.
- It is at least 32 bytes long.
- It is not one of the well-known committed default values.

If any check fails in Production, startup aborts with an *InvalidOperationException* stating that `Jwt:Key` must be a strong (≥32 byte) secret and must not be a committed default.

> 
> **Warning:** The placeholder keys shipped in `.env.example` and used for local development are explicitly blacklisted. They will pass the length check but are still rejected outside Development. Never reuse them.

## Generate a strong key

Produce a random secret of at least 32 bytes. For example:

    openssl rand -base64 48

Use the full output as the key value. Longer is fine; the minimum is 32 bytes.

## Provide the key by environment

### Docker Compose

Set `JWT_KEY` in your `.env` file. Both the development and production Compose files require it and fail fast if it is missing:

    JWT_KEY=Zx8v...your-48-byte-random-value...==

### Kubernetes

Store the key in the `mobilitymanager-secrets` Secret under `Jwt__Key`:

    kubectl -n mobilitymanager-prod create secret generic mobilitymanager-secrets \
      --from-literal=Jwt__Key='Zx8v...your-random-value...==' \
      --dry-run=client -o yaml | kubectl apply -f -

### Published deployment

Supply `Jwt__Key` as an environment variable from your process manager or secret store. Do not place it in a committed `appsettings.json`.

## Optional issuer and audience

You can also set `Jwt__Issuer` and `Jwt__Audience`. If omitted, they default to `CK.MobilityManager` and `CK.MobilityManager.Users` respectively. Keep these consistent across all instances that validate the same tokens.

> 
> **Note:** Changing the signing key invalidates all previously issued tokens, forcing users to sign in again. Rotate deliberately and roll the new key out to every instance at once.

## Related

- Configuring environment variables and the .env file
- Configuring the document encryption-at-rest key
- Production hardening checklist
- Troubleshooting a failed startup

# Kubernetes deployment overview

MobilityManager ships Kubernetes manifests under `k8s/`, organised as a Kustomize base with per-environment overlays. This article explains the structure and how to deploy the application to a cluster.

## Manifest layout

The application manifests live under `k8s/mobilitymanager`:

- **base/** — the Deployment, Service, and PodDisruptionBudget shared by all environments.
- **overlays/dev/** and **overlays/prod/** — environment-specific ConfigMap, Ingress, and Kustomize patches.

The base Deployment runs two replicas of the container on port 8080, with pod anti-affinity across zones and a PodDisruptionBudget that keeps at least one pod available.

## Configuration and secrets

Non-sensitive settings come from a ConfigMap (`mobilitymanager-config`) and secrets from a Secret (`mobilitymanager-secrets`), both injected via `envFrom`:

- **ConfigMap:** `ASPNETCORE_ENVIRONMENT`, `ASPNETCORE_URLS`, `DatabaseProvider`, `LicensePortal__BaseUrl`, and `DocumentStorage__EnableEncryptionAtRest`.
- **Secret:** `ConnectionStrings__SystemConnection`, `ConnectionStrings__TenantConnection`, and `Jwt__Key`.

Create or update the Secret without committing credentials. For example:

    kubectl -n mobilitymanager-prod create secret generic mobilitymanager-secrets \
      --from-literal=ConnectionStrings__SystemConnection='...' \
      --from-literal=ConnectionStrings__TenantConnection='...' \
      --from-literal=Jwt__Key='...' \
      --dry-run=client -o yaml | kubectl apply -f -

> 
> **Warning:** `secret.example.yaml` is a template only. Never commit real connection strings or signing keys; manage them through your cluster's secret store.

## Health probes and security context

The Deployment wires the built-in health endpoints to Kubernetes probes: `/livez` for liveness, `/readyz` for readiness, and `/healthz` as the startup probe. The pod runs hardened: a non-root numeric UID, a read-only root filesystem, dropped Linux capabilities, and no privilege escalation. Writable paths are provided as `emptyDir` volumes for `/tmp`, `/app/data`, and `/app/logs`.

## Deploy with Kustomize

1. Ensure the ConfigMap values and Secret exist in the target namespace.
2. Preview the rendered manifests:

        kubectl kustomize k8s/mobilitymanager/overlays/prod
3. Apply the overlay:

        kubectl apply -k k8s/mobilitymanager/overlays/prod
4. Watch the rollout:

        kubectl -n mobilitymanager-prod rollout status deployment/mobilitymanager

> 
> **Note:** The included Ingress uses a Cloudflare Tunnel ingress class and terminates TLS outside the pod. Replace the ingress class and hostnames to match your own cluster.

## Related

- Deployment architecture overview
- Configuring the database connection strings
- Understanding the health endpoints
- Production hardening checklist

# Running the platform with Docker Compose for development

The repository includes a Docker Compose stack for local development and testing. It runs a PostgreSQL database and the MobilityManager application (API plus bundled Angular UI) together, with an optional pgAdmin tool. This article walks through using it.

## What the stack includes

- **postgres** — a `postgres:16-alpine` database, exposed on port 5432, initialised with the system and tenant databases.
- **api** — the application built from the API service Dockerfile, exposed on port 8080, serving both the API and the UI.
- **pgadmin** — an optional database UI on port 5050, started only with the `tools` profile.

The API service runs with `ASPNETCORE_ENVIRONMENT=Production` and `DatabaseProvider=PostgreSQL`, with connection strings already pointing at the `postgres` service.

## Provide required settings

The Compose file requires a JWT signing key and accepts a database password. Create a `.env` file in the repository root:

    POSTGRES_PASSWORD=DevPassword123!
    JWT_KEY=YourSecretKeyThatMustBeAtLeast32CharactersLong!

> 
> **Warning:** `JWT_KEY` is mandatory — the stack runs as Production, and there is no committed fallback key, so Compose fails fast if it is missing. Do not reuse the example key from `.env.example`; it is a known value and is rejected outside Development.

## Start the stack

1. Build and start the services:

        docker compose up --build
2. Wait for the database health check to pass; the API waits for a healthy database before starting.
3. Open `http://localhost:8080` in your browser. On first run you are redirected to the setup wizard.

## Optional database tooling

To start pgAdmin alongside the stack, enable the `tools` profile:

    docker compose --profile tools up -d

pgAdmin is then available on `http://localhost:5050` using the credentials from `PGADMIN_EMAIL` and `PGADMIN_PASSWORD`.

## Check health and stop

The API container has a built-in health check that calls `/healthz`. You can verify it manually:

    curl http://localhost:8080/healthz

Stop and remove the containers when finished:

    docker compose down

> 
> **Note:** Database data persists in the `postgres_data` volume between runs. To start completely fresh, remove volumes with `docker compose down -v`.

## Related

- Running the platform with Docker Compose in production
- Configuring environment variables and the .env file
- Setting the JWT signing key for production
- Understanding the health endpoints

# Running the platform with Docker Compose in production

For production Compose deployments, MobilityManager provides an override file, `docker-compose.prod.yml`, that layers on top of the base Compose file. It runs a prebuilt image from a registry instead of building locally and enforces stricter settings.

## How the override differs

Compared with the development stack, the production override:

- Runs a **prebuilt image** pulled from a registry rather than building from source.
- Requires `POSTGRES_PASSWORD` and `JWT_KEY` — startup fails fast if either is missing.
- Sets `restart: always` on both services and applies memory limits and reservations.
- Accepts an optional `License__PublicKey` for license validation.

## Prepare the environment file

Create a `.env` file with strong, unique secrets and the image coordinates:

    POSTGRES_PASSWORD=YourStrongPassword123!
    JWT_KEY=YourSecretKeyThatMustBeAtLeast32CharactersLong!
    JWT_ISSUER=CK.MobilityManager
    JWT_AUDIENCE=CK.MobilityManager.Users
    DOCKER_REGISTRY=ghcr.io
    DOCKER_IMAGE=your-org/ck-mobility-manager
    VERSION=v1.25.0

> 
> **Tip:** Pin `VERSION` to a specific tag in production rather than `latest`, so redeployments are reproducible and upgrades are deliberate.

## Deploy

1. Combine the base and production files and start in the background:

        docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
2. Verify the application is healthy:

        curl http://localhost:8080/healthz
3. Access the application at `http://localhost:8080` and complete onboarding if this is a fresh database.

> 
> **Warning:** Terminate TLS in front of the container (reverse proxy or load balancer). The container serves plain HTTP on port 8080 and should never be exposed directly to the internet.

## Upgrading

1. Update `VERSION` in `.env` to the new tag.
2. Pull the new image:

        docker compose -f docker-compose.yml -f docker-compose.prod.yml pull
3. Recreate the services:

        docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
4. Confirm health with `curl http://localhost:8080/healthz`. Pending migrations are applied automatically as the new container starts.

## Related

- Running the platform with Docker Compose for development
- Setting the JWT signing key for production
- Production hardening checklist
- Backing up and restoring your databases

# Choosing between PostgreSQL and SQL Server

MobilityManager supports two relational database providers: PostgreSQL and Microsoft SQL Server. You select the provider once through the `DatabaseProvider` setting, and it applies to both the system and tenant databases. This article helps you choose and explains how the setting behaves.

## The DatabaseProvider setting

The provider is read from configuration at runtime. Valid values are exactly:

- `PostgreSQL`
- `SqlServer`

Set it as an environment variable (`DatabaseProvider`) or in `appsettings.json`. Any other value causes the application to throw an *Unsupported database provider* error at startup, so spelling matters.

> 
> **Note:** Both the system database and every tenant database use the same provider. You cannot mix PostgreSQL for one and SQL Server for the other in a single deployment.

## When no provider is configured

If neither a provider nor a connection string is present, each database context falls back to an in-memory database. This lets the application start unconfigured and present the onboarding wizard rather than crashing. In-memory mode is never treated as "configured" and must not be used for real data.

## PostgreSQL

PostgreSQL is the default and recommended option, particularly on ARM-based machines and for the Docker Compose stacks, which ship a `postgres:16-alpine` container. Connection strings use the Npgsql key/value format:

    Host=your-db;Database=mobilitymanager_system;Username=app;Password=secret

## SQL Server

Microsoft SQL Server is fully supported and is used by the hosted demo environment on Azure SQL. Connection strings use the standard SQL Server format:

    Server=tcp:your-server,1433;Initial Catalog=MobilityManagerSystem;User ID=app;Password=secret;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;

## Comparison at a glance

| Consideration | PostgreSQL | SQL Server |
| --- | --- | --- |
| DatabaseProvider value | `PostgreSQL` | `SqlServer` |
| Recommended for | New installs, ARM hosts, Compose | Existing SQL Server estates, Azure SQL |
| Connection format | Npgsql (Host/Database) | Server/Initial Catalog |
| Bundled in Compose | Yes | No (bring your own) |

## Choosing for your environment

1. Pick the provider your team already operates and can back up confidently.
2. Set `DatabaseProvider` to the matching value on every instance and background job.
3. Provide `SystemConnection` and `TenantConnection` strings in the same format as the provider.
4. Keep the provider consistent across your API pods, seed jobs, and Compose services.

> 
> **Warning:** Migrations are provider-specific. Switching providers after go-live is a data migration project, not a configuration change — decide before you load production data.

## Related

- Configuring the database connection strings
- Using the LocalDatabase provider mode
- Migrations and the onboarding redirect on startup
- Backing up and restoring your databases

# Configuring the database connection strings

MobilityManager separates data into a system database and one or more tenant databases. You point the application at them with two named connection strings: `SystemConnection` and `TenantConnection`. This article explains what each does and how to set them.

## The two canonical connection strings

| Name | Purpose |
| --- | --- |
| `SystemConnection` | System database: tenant registry, users, roles, and license. |
| `TenantConnection` | Tenant database used for onboarding and migrations of operational data. |

The AppHost and Docker Compose both use `SystemConnection` and `TenantConnection` as the canonical names. A legacy `DefaultConnection` value is also recognised as a fallback for the tenant connection, but new deployments should use the canonical names.

> 
> **Note:** At runtime, individual tenants are resolved dynamically from a connection-string template stored in the system database. `TenantConnection` is primarily used for onboarding and applying migrations to the tenant schema.

## Setting connection strings as environment variables

In configuration, connection strings live under the `ConnectionStrings` section. As environment variables, use the double-underscore separator:

    ConnectionStrings__SystemConnection=...
    ConnectionStrings__TenantConnection=...

### PostgreSQL format

    ConnectionStrings__SystemConnection=Host=postgres;Database=mobilitymanager_system;Username=mobilitymanager;Password=YourStrongPassword
    ConnectionStrings__TenantConnection=Host=postgres;Database=mobilitymanager_tenant;Username=mobilitymanager;Password=YourStrongPassword

### SQL Server format

    ConnectionStrings__SystemConnection=Server=tcp:your-server,1433;Initial Catalog=MobilityManagerSystem;User ID=app;Password=secret;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;
    ConnectionStrings__TenantConnection=Server=tcp:your-server,1433;Initial Catalog=MobilityManagerTenant;User ID=app;Password=secret;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;

> 
> **Warning:** The connection-string format must match the `DatabaseProvider` value. A PostgreSQL string with `DatabaseProvider=SqlServer` (or vice versa) will fail to connect.

## Where to store them by environment

- **Docker Compose:** the Compose files build these strings from the `POSTGRES_PASSWORD` variable and the `postgres` service host.
- **Kubernetes:** place them in the `mobilitymanager-secrets` Secret as `ConnectionStrings__SystemConnection` and `ConnectionStrings__TenantConnection`, not in the ConfigMap.
- **Published deployment:** supply them as environment variables from your process manager or secret store.

## Unconfigured behaviour

If a connection string or the provider is missing, the application uses an in-memory database and shows the onboarding wizard instead of failing. Once you provide valid strings and a provider, migrations are applied on startup.

## Related

- Choosing between PostgreSQL and SQL Server
- Configuring environment variables and the .env file
- Migrations and the onboarding redirect on startup
- Backing up and restoring your databases

# Migrations and the onboarding redirect on startup

When MobilityManager starts, it tries to bring the database schema up to date and redirects users to the setup wizard if the system has not been configured yet. Understanding this startup behaviour helps you interpret logs and first-run experiences.

## Migrations are applied on startup

On boot, the application runs its database migration routine. It first migrates the system database, then iterates through every active tenant that has a connection-string template and migrates each tenant database in turn.

The process is deliberately fault-tolerant:

- If the database is not reachable or not yet configured, migrations are **skipped** and a warning is logged — the app still starts so the onboarding wizard can run.
- Non-relational providers (the in-memory fallback used before configuration) are skipped entirely.
- A connectivity check runs before migrating, so an unreachable database logs a clear message rather than crashing startup.

> 
> **Note:** Because migrations run automatically at startup, a rolling deployment applies pending schema changes as the new instance comes up. Plan schema changes to be backward compatible during rollout.

## What "configured" means

The application treats itself as configured only when all of the following are true in a reachable, non-in-memory database:

1. At least one user exists.
2. An active license is present.
3. At least one tenant exists.

Until then, the system is considered unconfigured.

## The onboarding redirect

An onboarding middleware inspects incoming browser requests. If the system is not configured, it redirects the user to `/setup` so they can complete the setup wizard. The redirect is skipped for paths that must always work, including:

- API calls under `/api/`
- The setup and login pages (`/setup`, `/login`)
- Health endpoints (`/healthz`, `/livez`, `/readyz`)
- Framework and static asset paths, and any request for a file (paths containing a dot)

> 
> **Tip:** Health probes are exempt from the redirect, so Kubernetes and Compose health checks keep passing even while the system is waiting to be configured.

## First-run sequence

1. Start the app with a valid `DatabaseProvider` and connection strings pointing at empty databases.
2. Startup migrations create the system schema; with no tenants yet, tenant migrations are skipped.
3. A browser request is redirected to `/setup`.
4. You complete the wizard, which creates the first admin user, activates the license, and provisions the first tenant.
5. Subsequent requests are served normally; the redirect no longer fires.

## Related

- Configuring the database connection strings
- Choosing between PostgreSQL and SQL Server
- Understanding the health endpoints
- Troubleshooting a failed startup

# Using the LocalDatabase provider mode

When you run MobilityManager locally through the Aspire AppHost, the `LocalDatabase:Provider` setting controls how the development database is provisioned. This is separate from the runtime `DatabaseProvider` setting and only applies to the AppHost orchestration used during development.

## What LocalDatabase:Provider does

The AppHost reads `LocalDatabase:Provider` to decide whether to start a managed database container for you:

- **PostgreSQL** (default) — the AppHost starts a PostgreSQL resource and creates two databases, wiring them to the `SystemConnection` and `TenantConnection` names automatically.
- **SqlServer** — the AppHost does *not* provision a database. The API keeps using your own external or locally installed SQL Server through the connection strings you supply.

Any other value throws an *Unsupported LocalDatabase:Provider* error listing the supported values.

> 
> **Note:** The default AppHost mode is PostgreSQL, which is the recommended local option on ARM-based machines. The setting lives in `src/CK.MobilityManager.AppHost/appsettings.Development.json`.

## Running in PostgreSQL mode

In PostgreSQL mode the AppHost provisions the databases `mobilitymanager_system` and `mobilitymanager_tenant`, sets `DatabaseProvider=PostgreSQL` on the API, and waits for the databases before starting. Provide a password for the PostgreSQL parameter, then run the AppHost:

    $env:Parameters__postgres-password="DevPassword123!"
    dotnet run --project src/CK.MobilityManager.AppHost

Startup, onboarding, and tenant provisioning apply normal EF Core migrations on PostgreSQL just as they do on SQL Server.

## Running in SQL Server mode

To keep using a locally managed SQL Server instance, override the provider:

    $env:LocalDatabase__Provider="SqlServer"
    dotnet run --project src/CK.MobilityManager.AppHost

In this mode you are responsible for the SQL Server instance and for supplying the `SystemConnection` and `TenantConnection` strings the API uses.

> 
> **Tip:** Environment variables use the double-underscore separator to represent nested keys, so `LocalDatabase__Provider` maps to the `LocalDatabase:Provider` configuration value.

## How it relates to production

The `LocalDatabase:Provider` switch is a development convenience for the Aspire AppHost only. In Docker Compose, Kubernetes, or a published deployment there is no AppHost: you set the runtime `DatabaseProvider` directly and point the connection strings at your own managed databases. The AppHost and Docker Compose both use `SystemConnection` and `TenantConnection` as the canonical connection string names.

## Related

- Prerequisites for building and running the platform
- Choosing between PostgreSQL and SQL Server
- Configuring the database connection strings
- Running the platform with Docker Compose for development

# Backing up and restoring your databases

MobilityManager keeps all persistent data in its databases: a system database and one or more tenant databases. Protecting that data means backing up both, plus the secrets needed to read it. This article gives practical backup and restore guidance.

## What to back up

- **System database** (`SystemConnection`) — the tenant registry, users, roles, and license.
- **Tenant database(s)** (`TenantConnection` and per-tenant databases) — fleet, booking, and operational data.
- **Secrets** — the `Jwt__Key` and the `DocumentStorage__LocalEncryptionKey`.
- **Stored documents** — any local document storage directory, if you use local storage.

> 
> **Warning:** A database backup is not enough on its own. If you lose the document encryption key, encrypted documents in a restored database cannot be decrypted. Back up keys separately and securely.

## Backing up PostgreSQL

Use `pg_dump` for each database. For the Docker Compose stack, run it inside the database container:

    docker exec mobilitymanager-postgres \
      pg_dump -U mobilitymanager mobilitymanager_system > system-backup.sql
    
    docker exec mobilitymanager-postgres \
      pg_dump -U mobilitymanager mobilitymanager_tenant > tenant-backup.sql

## Backing up SQL Server

For SQL Server, use native `BACKUP DATABASE` statements or your platform's managed backups. On Azure SQL, rely on automated backups and point-in-time restore, and export a `.bacpac` for portable copies.

> 
> **Tip:** Keep the backup provider consistent with your `DatabaseProvider`. You cannot restore a PostgreSQL dump into SQL Server or vice versa without a data migration.

## Restoring

1. Provision empty target databases matching your provider.
2. Restore the system database first, then the tenant database(s).

        psql -U mobilitymanager -d mobilitymanager_system < system-backup.sql
        psql -U mobilitymanager -d mobilitymanager_tenant < tenant-backup.sql
3. Restore the `Jwt__Key` and document encryption key to their secret store.
4. Point `SystemConnection` and `TenantConnection` at the restored databases and start the application.
5. On startup the app applies any pending migrations, so a backup from an older schema is brought up to date automatically.

## Verify the restore

- Check `/healthz` returns 200.
- Sign in and confirm tenants, users, and recent records are present.
- Open an encrypted document to confirm the encryption key was restored correctly.

> 
> **Note:** Test restores on a regular schedule against a non-production target. An untested backup is not a reliable backup.

## Related

- Configuring the database connection strings
- Configuring the document encryption-at-rest key
- Migrations and the onboarding redirect on startup
- Production hardening checklist

# Troubleshooting a failed startup

If MobilityManager fails to start or never becomes ready, the cause is usually a missing secret, a database it cannot reach, or a misconfigured provider. This article maps the most common failures to their fixes.

## Start with the logs and health endpoints

1. Read the container or process logs for the first error — startup checks fail fast with a clear message.
2. Check the health endpoints:

        curl -i http://localhost:8080/livez
        curl -i http://localhost:8080/readyz

A liveness pass with a readiness failure points to a dependency problem (typically the database) rather than a crash.

## The JWT key is missing or weak

**Symptom:** startup throws an error that `Jwt:Key` must be a strong (≥32 byte) secret and must not be a committed default.

**Fix:** set `Jwt__Key` to a fresh random value of at least 32 bytes that is not one of the example keys. In Docker Compose this is `JWT_KEY`; in Kubernetes it is a Secret entry.

## The document encryption key is missing

**Symptom:** startup throws an error that `DocumentStorage:EnableEncryptionAtRest` is on but `DocumentStorage:LocalEncryptionKey` is not configured.

**Fix:** provide a base64-encoded 32-byte key in `DocumentStorage__LocalEncryptionKey`, or set `DocumentStorage__EnableEncryptionAtRest=false` if you do not need encryption.

## The database provider is invalid

**Symptom:** an *Unsupported database provider* error.

**Fix:** set `DatabaseProvider` to exactly `PostgreSQL` or `SqlServer`. The value is case-sensitive.

## The app starts but redirects everyone to /setup

**Symptom:** the application starts and stays healthy but every page redirects to the setup wizard.

**Cause:** the system is not yet configured, or the app is running against the in-memory fallback database because the connection strings or provider are missing.

**Fix:** confirm `DatabaseProvider` and both connection strings are set and reachable, then complete onboarding. The app is "configured" only once it has a user, an active license, and a tenant.

> 
> **Note:** Migrations are skipped — not failed — when the database is unreachable at startup. Look for a warning about applying migrations and using the onboarding wizard, and fix connectivity rather than the migration itself.

## The database is unreachable

- Verify the host, port, database name, username, and password in the connection string.
- Confirm the connection-string format matches the provider (Npgsql for PostgreSQL, Server/Initial Catalog for SQL Server).
- Check network policy and that the database container or service is healthy.
- For SQL Server, confirm `Encrypt` and certificate trust settings are compatible with your server.

## Clock tampering aborts startup

**Symptom:** a critical log entry states clock tampering was detected and startup was aborted.

**Fix:** the system clock appears to have moved backwards relative to the license record. Restore the correct system time and restart.

## Related

- Setting the JWT signing key for production
- Configuring the document encryption-at-rest key
- Configuring the database connection strings
- Understanding the health endpoints

# Understanding the health endpoints

MobilityManager exposes health endpoints that let orchestrators, load balancers, and monitoring tools check whether the application is alive and ready to serve traffic. This article describes each endpoint and how to use them.

## The endpoints

| Endpoint | Meaning | Typical use |
| --- | --- | --- |
| `/healthz` | Combined health — all checks must pass. | Startup probe, general health check. |
| `/livez` | Liveness — only checks tagged *live* must pass. | Liveness probe: is the process responsive? |
| `/readyz` | Readiness — only checks tagged *ready* must pass. | Readiness probe: can it accept traffic? |
| `/health` | Combined health, equivalent to `/healthz`. | Alternative health path. |

All of these are available in every environment, because production monitoring depends on them. Each returns HTTP 200 when healthy and a non-200 status when a required check fails.

## Liveness versus readiness

- **Liveness** (`/livez`) answers "is the app running?" A persistent failure signals the orchestrator to restart the container.
- **Readiness** (`/readyz`) answers "can the app accept traffic right now?" A failure removes the pod from the load balancer without restarting it.
- **Combined** (`/healthz`) requires every check to pass and is well suited to a startup probe that gates traffic until the app has fully initialised.

> 
> **Note:** Health-check requests are excluded from distributed tracing, so probing frequently does not add telemetry noise.

## How Kubernetes uses them

The provided Kubernetes Deployment maps the endpoints to probes:

- **livenessProbe** → `/livez`
- **readinessProbe** → `/readyz`
- **startupProbe** → `/healthz`

The startup probe gives the app time to initialise before liveness and readiness checks take over. Docker Compose likewise uses `/healthz` for its container health check.

## Checking health manually

    curl -i http://localhost:8080/healthz
    curl -i http://localhost:8080/livez
    curl -i http://localhost:8080/readyz

A healthy instance returns `200 OK` for each. A readiness failure with a passing liveness check usually means the app is up but a dependency (such as the database) is not yet available.

> 
> **Tip:** The onboarding redirect never intercepts these paths, so probes keep succeeding even while the system is unconfigured and waiting for the setup wizard.

## Related

- Kubernetes deployment overview
- Migrations and the onboarding redirect on startup
- Troubleshooting a failed startup
- Production hardening checklist

# Deployment architecture overview

MobilityManager ships as a single deployable unit: the .NET 10 API service hosts both the REST API and the compiled Angular 19 user interface. Understanding this model helps you plan hosting, scaling, and reverse-proxy configuration correctly.

## One process serves both API and UI

During a Release build, the Angular application in `src/web/UI` is compiled and copied into the API project's `wwwroot` folder. At runtime, one Kestrel process serves everything from a single port (8080 in containers):

- **API routes** (for example `/api/...`, `/healthz`) are matched first.
- **Static assets** (JavaScript, CSS, images) are served from `wwwroot`.
- **Unmatched routes** fall back to `index.html`, so Angular client-side routing works on deep links and page refreshes.

> 
> **Note:** Because the UI and API share an origin, you do not need to configure CORS between them in a standard deployment. There is no separate web server to run or scale.

## How the bundle is produced

The build behaviour depends on configuration:

- **Development builds** do not build the Angular UI, keeping compile times fast.
- **Release builds** automatically build and bundle the UI.
- You can force a UI build in any configuration with `/p:BuildAngularUI=true`.

To produce a deployable output, publish only the API project:

    dotnet publish src/CK.MobilityManager.ApiService -c Release -o ./publish

The published folder contains the .NET binaries and the Angular assets in `wwwroot`, ready to run behind a reverse proxy or inside a container.

## Multi-tenant data layer

The application separates data into two databases, addressed by named connection strings:

- **SystemConnection** — the system database holding the tenant registry, users, roles, and license.
- **TenantConnection** — the tenant database holding fleet, booking, and operational data.

Both PostgreSQL and Microsoft SQL Server are supported through the `DatabaseProvider` setting. Roles are fixed at three levels: SystemAdmin, FleetManager, and Driver.

## Deployment options

You can run the single deployment in several ways, all covered by dedicated articles:

- Docker Compose for local evaluation and small deployments.
- A prebuilt container image for production Compose stacks.
- Kubernetes using the provided Kustomize manifests.
- A published folder run directly with `dotnet` behind your own reverse proxy.

> 
> **Tip:** In every deployment mode the container or process listens on a single HTTP port. Terminate TLS at your reverse proxy, load balancer, or ingress and forward plain HTTP to the app.

## Related

- Prerequisites for building and running the platform
- Running the platform with Docker Compose in production
- Kubernetes deployment overview
- Migrations and the onboarding redirect on startup

# Prerequisites for building and running the platform

Before you build or deploy MobilityManager, make sure your workstation or build agent has the required toolchain. This article lists the prerequisites for building from source and the options for running the result.

## Build prerequisites

To compile the platform from source you need:

- **.NET 10 SDK** — builds the API service and shared libraries.
- **Node.js 18+ and npm** — builds the Angular 19 user interface that is bundled into the API.

The Angular UI is only compiled during Release builds or when you explicitly request it, so Node.js is not needed for fast Development-only backend builds.

> 
> **Note:** If you deploy a prebuilt container image instead of building from source, you do not need the .NET SDK or Node.js on the target host — only a container runtime such as Docker.

## Verify your tools

1. Confirm the .NET SDK is installed:

        dotnet --version
2. Confirm Node.js and npm are installed:

        node --version
        npm --version

## Building the solution

Use the configuration that matches your goal:

- **Development (fast, no UI build):**

        dotnet build
- **Production (includes the Angular UI):**

        dotnet build -c Release
- **Force the UI build in Debug mode:**

        dotnet build /p:BuildAngularUI=true

## Running for local development

The quickest path is Visual Studio 2022: open `CK.MobilityManager.slnx`, set `CK.MobilityManager.AppHost` as the startup project, and press **F5**. The Aspire AppHost starts PostgreSQL, the API service, and the Angular UI with hot reload.

From the command line, provide a database password and run the AppHost:

    $env:Parameters__postgres-password="DevPassword123!"
    dotnet run --project src/CK.MobilityManager.AppHost

You can also run the API on its own (serving the bundled UI) with `dotnet run --project src/CK.MobilityManager.ApiService`, or run the Angular dev server separately from `src/web/UI` with `npm start` for hot-reload UI work.

## Container and cluster runtimes

For containerised deployments you additionally need:

- **Docker** with Compose v2 for the Docker Compose stacks.
- **kubectl** (with Kustomize built in) for Kubernetes deployments.

> 
> **Tip:** PostgreSQL is the recommended local database, especially on ARM-based machines. SQL Server remains fully supported for teams that already run it.

## Related

- Deployment architecture overview
- Choosing between PostgreSQL and SQL Server
- Using the LocalDatabase provider mode
- Running the platform with Docker Compose for development

# Encryption at rest and the encryption key

This article explains how MobilityManager encrypts stored documents, where the encryption key comes from, and the one rule you must never break when rotating it.

## Encryption at rest, on every provider

When encryption at rest is enabled, each file is encrypted **before** it is written to storage. This applies no matter which provider is active — Local, Azure Blob, or S3 all receive already-encrypted content. As a result, the folder, container, or bucket never holds a readable copy of your documents. Files are decrypted only when a user with permission downloads them. Encryption at rest is enabled by default.

## How the encryption works

The platform uses **AES-GCM**, an authenticated encryption scheme, and wraps each file in a self-describing envelope. Because the envelope is authenticated, tampering with a stored file is detected at download time and the file will not decrypt. Every file gets a fresh random nonce, so identical files do not produce identical ciphertext.

## The encryption key

Encryption uses a single 256-bit key, supplied as a base64-encoded 32-byte value. There are two ways to provide it:

| Source | How it is set |
| --- | --- |
| Database key | An operator sets a base64 key directly in the storage settings. This is preferred when present. |
| Configuration secret reference | A reference to a configuration key (for example an environment variable or secret store entry) that holds the key. |

If a database key is set, it takes priority; otherwise the platform resolves the key from the configured secret reference. For security, the raw key is never returned by the settings screen — the interface only indicates whether a key is present so it can offer rotation.

## Rotating the key safely

The platform uses one active key with no key-ring and does **not** re-encrypt existing files when the key changes. This has a critical consequence:

> **Warning:** If you change the encryption key, documents encrypted under the previous key can no longer be decrypted. There is no automatic re-encryption. Treat key changes as a one-way event and confirm you have no important documents under the old key before rotating.

To protect you from accidental loss, saving storage settings without providing a new key **preserves** the existing key rather than clearing it. The key only changes when you deliberately enter a new one. A new key must be a valid base64-encoded 32-byte value, or the save is rejected.

## Backward compatibility

Files uploaded before encryption was enabled remain readable. On download, the platform detects whether a stored file is encrypted by checking the envelope, and returns plaintext files unchanged. This means enabling encryption does not break access to older documents — but only files written after it is enabled are actually encrypted.

> **Note:** Managing the encryption key requires the **settings:manage** permission, typically held by SystemAdmin. Store a secure backup of the key outside the platform, because losing it means losing access to every document encrypted under it.

## Related

- Where documents are stored: Local, Azure Blob, and S3 providers
- Configuring the document storage provider
- Tenant scoping and document isolation
- Document retention and permanent deletion

# Document retention and permanent deletion

This article explains how long documents stay in MobilityManager, how versions accumulate, and what permanent deletion means, so you can manage document lifecycle deliberately.

## Documents persist until you remove them

The shared DocumentModule does not automatically expire or purge uploaded files. A document stays available for as long as its parent record exists and until someone with the right permission deletes it. There is no built-in scheduled clean-up that removes documents after a set period, so retention is a matter of deliberate action, not automatic aging.

## Versions accumulate over time

Because each upload of a replacement creates a new version rather than overwriting the previous one, a single document can hold several versions. Downloads always return the latest, but earlier versions remain part of the document's history. This is intentional — it preserves an evidence trail — but it means storage use grows with each replacement.

> **Tip:** If you frequently re-upload corrected scans, remember that older versions are retained. Upload a clean, final copy once where you can, rather than many revisions, to keep history tidy.

## Deletion is permanent and removes all versions

Deleting a document removes the document record and every one of its versions from your company's database in a single operation. There is no recycle bin and no undo within the DocumentModule. After deletion the file no longer appears in any list and cannot be downloaded through the platform.

| Action | Effect on retention |
| --- | --- |
| Upload a replacement | Adds a new version; nothing is removed. |
| Deactivate a category | Stops new uploads under that label; existing documents are untouched. |
| Delete a document | Permanently removes the document and all its versions. |

## Deletion is tied to the parent record

You remove a document through the record it is attached to — a vehicle, damage case, Beleg (receipt/expense document), or Strafmandat (penalty notice). This keeps deletion intentional and scoped: you cannot delete a file except through the record that owns it, which reduces the chance of removing the wrong document.

> **Warning:** Documents often serve as evidence for damage claims, penalty notices, and expenses. Before deleting, confirm the file is genuinely no longer needed and that no open case, audit, or legal obligation depends on it. Once the versions are removed, the content cannot be recovered from the platform.

## Planning your own retention policy

Because the platform will not delete documents for you, define a policy your team follows:

1. Decide how long each document type must be kept to meet your legal and tax obligations.
2. Assign the **documents:manage** permission only to the roles responsible for that clean-up.
3. Review and remove documents whose parent records are closed and whose retention period has passed.

> **Note:** Deleting a document removes it from the platform's records. Coordinate document retention with your organization's broader data-retention and privacy obligations.

## Related

- Deleting a document
- How documents work across the platform
- Tenant scoping and document isolation
- Encryption at rest and the encryption key

# Tenant scoping and document isolation

MobilityManager is multi-tenant: several companies share one platform without seeing each other's data. This article explains how documents are isolated to your company and what enforces that boundary.

## What "tenant" means here

A tenant is a company (client) on the platform. Each tenant has its own database, and document records — their metadata and version history — live in that tenant's database. There is no shared pool of documents across companies; a document belongs to exactly one tenant.

## Every document operation requires a tenant context

Before any upload, download, metadata listing, or deletion runs, the platform validates that your session has an active tenant context and that the tenant's database is reachable. If either check fails, the operation stops immediately with "Tenant context is required for document operations." No file is read or written without a resolved tenant.

> **Note:** This check is mandatory and applies to all document actions. It is enforced on the server, so it cannot be bypassed from the interface.

## Two layers of protection

| Layer | What it enforces |
| --- | --- |
| Permissions | **documents:view** to list and download, **documents:manage** to upload and delete. Checked server-side on every request. |
| Tenant scope | Requests run against your tenant's database only. Queries also match the parent record (its type and ID), so a document is reachable only through the record it belongs to. |

Together these mean that having permission is not enough to reach another company's files — the tenant context and the database boundary keep operations inside your own company.

## How storage backends stay opaque

File content is stored under server-generated random names, not original file names or any customer-identifying text. Even where multiple tenants share the same storage backend (for example one bucket), stored objects are opaque identifiers, and the mapping from a document to its stored file exists only inside the owning tenant's database. Combined with encryption at rest, the storage layer holds no readable, attributable content.

## What is shared and what is not

| Shared across the platform | Isolated per tenant |
| --- | --- |
| Document categories (the system-wide label list) | The documents themselves and their versions |
| The active storage provider configuration | Which record each document is attached to |
| The encryption setting and key | Uploader identity and audit details |

In other words, administrators define one set of categories and one storage configuration for everyone, but the files and their metadata never cross the tenant boundary.

> **Tip:** If a user suddenly cannot see documents they could see before, check whether their active company changed. Documents are only visible while the matching tenant context is active in the session.

## Related

- How documents work across the platform
- Encryption at rest and the encryption key
- Where documents are stored: Local, Azure Blob, and S3 providers
- Document retention and permanent deletion

# Configuring the document storage provider

This article shows administrators how to configure which provider MobilityManager uses to store uploaded files, and which settings each provider needs.

## Who can change storage settings

Storage configuration requires the **settings:manage** permission, typically held by SystemAdmin. There is a single, platform-wide storage configuration, and only one provider is active at a time. If no configuration exists yet, the platform starts with the Local provider and a default base folder.

## How secrets are handled

Sensitive values — Azure connection strings, S3 access and secret keys — are never typed directly into the storage settings. Instead you supply a **secret reference**: the configuration key under which the real secret is provided (for example through environment variables or a secret store). The platform resolves the reference at runtime. If a reference cannot be resolved, saving fails with a clear message so you never end up with a half-configured provider.

## Configure the Local provider

1. Set the provider to **Local**.
2. Enter a **base path** for stored files. This is required. An absolute path is used as-is; a relative path is rooted under the server's temporary directory.
3. Save. The platform verifies the folder exists (creating it if needed) and confirms it is writable with a quick write-and-delete probe before accepting the setting.

## Configure the Azure Blob provider

1. Set the provider to **AzureBlob**.
2. Enter the **container name** (required).
3. Enter the **connection string secret reference** (required) — the config key that holds your Azure Storage connection string.
4. Optionally enter a blob service URI.
5. Save. The container is created with no public access on first use if it does not already exist.

## Configure the S3 provider

1. Set the provider to **S3**.
2. Enter the **bucket name** and **region** (both required).
3. Enter the **access key secret reference** and **secret key secret reference** (both required).
4. Optionally enter a **service URL** for S3-compatible stores such as MinIO; this enables path-style addressing.
5. Save.

| Provider | Required fields |
| --- | --- |
| Local | Base path |
| AzureBlob | Container name, connection string secret reference |
| S3 | Bucket name, region, access key secret reference, secret key secret reference |

> **Warning:** Switching providers changes where *new* files are written and where downloads are read from. The platform keeps a single active configuration and does not migrate existing files between providers. Plan a provider change carefully so previously stored files remain reachable.

> **Note:** When you switch provider, fields for the other providers are cleared. Re-enter the details if you switch back. Encryption settings are handled separately from provider choice — see the encryption article.

## Related

- Where documents are stored: Local, Azure Blob, and S3 providers
- Encryption at rest and the encryption key
- How documents work across the platform
- Tenant scoping and document isolation

# Where documents are stored: Local, Azure Blob, and S3 providers

This article explains where MobilityManager keeps the actual file content you upload, and how the three storage providers — Local, Azure Blob, and S3 — differ.

## Metadata and file content are stored separately

Every document has two parts. The **metadata** (file name, content type, size, category, uploader, versions) lives in your company's database. The **file content** is written to a storage provider chosen by your administrator. A version record links the two by keeping the storage path where the content was written.

Regardless of provider, files are saved under server-generated names — a random identifier plus the original extension — not their original file names. The friendly file name is preserved in the metadata and reapplied when you download.

## The three providers

| Provider | Where files go | Best for |
| --- | --- | --- |
| **Local** | A directory on the server's file system (the configured base path). | On-premises installs or evaluation, where files stay on your own server. |
| **Azure Blob** | A container in an Azure Storage account. | Deployments on Microsoft Azure. |
| **S3** | An Amazon S3 bucket (or an S3-compatible store). | Deployments on AWS or S3-compatible object storage such as MinIO. |

## Local provider

The Local provider writes each file into a configured base folder on the server. If the folder is missing it is created, and the platform verifies the folder is writable before accepting it. Reads are guarded against path traversal, so only files inside the configured storage root can be served.

## Azure Blob provider

The Azure Blob provider uploads each file as a blob in a named container. If the container does not exist yet, it is created with no public access, so blobs are never anonymously reachable. The connection to Azure is resolved from a secret reference rather than a value typed into the app.

## S3 provider

The S3 provider stores each file as an object in a bucket, in a specified region. It also supports a custom service URL, which enables path-style addressing for S3-compatible stores like MinIO. Access and secret keys are resolved from secret references, not stored in plain text.

> **Note:** There is one active storage configuration for the whole platform at a time. The active provider is where new files are written and where downloads are read from.

## Encryption is independent of the provider

When encryption at rest is enabled, files are encrypted *before* they are handed to whichever provider is active. That means Local, Azure, and S3 all receive already-encrypted content, and the underlying folder, container, or bucket never holds a readable copy. Decryption happens only when you download.

> **Tip:** Choosing a provider is a platform-level decision made by an administrator with **settings:manage**. As an everyday user you do not pick where files go — you just upload, and the active provider handles the rest.

## Related

- Configuring the document storage provider
- Encryption at rest and the encryption key
- How documents work across the platform
- Tenant scoping and document isolation

# Attaching documents to vehicles, damage cases, penalty notices, and receipts

This article explains how the same document tools attach files to different kinds of records — vehicles, damage cases, penalty notices, and receipts — and what is unique about each.

## The same engine, different parents

Every attachment in MobilityManager flows through the shared DocumentModule. You always attach a file from inside a record, and the record supplies its **association type** and **ID** automatically. The upload, category, size, versioning, and permission rules are identical everywhere. What differs is which parent record the file is linked to and any parent-specific validation.

## Supported record types

| Record | Association type | Typical use |
| --- | --- | --- |
| Vehicle | `Vehicle` | Registration papers, contracts, and inspection certificates in the Fahrzeugakte (vehicle file). |
| Damage case | `DamageCase` | Photos of the damage and repair estimates. |
| Penalty notice | `Mandate` | Scans of the Strafmandat (penalty notice). |
| Receipt / expense | `Beleg` | The receipt or invoice behind a Beleg (receipt/expense document). |
| Driver's licence | `driver-license` | Licence images for Führerscheinkontrolle (driver's licence check). |
| Compliance report | `license-compliance-report` | Generated licence-compliance reports. |

## Attach a file to any record

1. Open the specific record — the vehicle, damage case, penalty notice, or Beleg.
2. Open its documents area and choose the file.
3. Pick a required **category**.
4. Upload. The file is stored as version 1 and linked to that record.

You need the **documents:manage** permission to attach, and **documents:view** to see and open existing attachments.

## Parent-specific validation

Because a file must belong to a real record, each area checks the parent before storing. For example, uploading a vehicle document to a vehicle that does not exist is rejected with "Vehicle not found." Similarly, Beleg, damage case, and penalty notice uploads fail if the parent record is missing. This keeps orphaned files from accumulating.

> **Note:** You cannot attach a file to a record type the module does not recognize. Unsupported types return "Document operation unavailable", which is why attachments are always started from within a record rather than a standalone upload page.

## Penalty notices can also ingest from a URL

Most record types accept direct file uploads. Penalty notices additionally support ingesting a document from an external URL: the platform downloads the file and stores it internally, recording its source as URL ingestion rather than a manual upload. The result is an ordinary document you can view and download like any other.

> **Tip:** A cross-reference field lets you tag an attachment with a related Beleg reference. Use it to connect, say, a repair invoice on a damage case to the Beleg that pays it, so reviewers can trace the paperwork.

## Related

- How documents work across the platform
- Uploading a document and file size limits
- Downloading a document
- Tenant scoping and document isolation

# Deleting a document

This article shows you how to delete a document from a record in MobilityManager and explains exactly what deletion removes.

## Before you start

Deleting requires the **documents:manage** permission, typically held by FleetManager and SystemAdmin. As with all document operations, you also need an active company (tenant) in your session.

## Delete a document

1. Open the record the file is attached to — for example a vehicle, a Beleg (receipt/expense document), a damage case, or a Strafmandat (penalty notice).
2. Open the record's documents area and locate the file in the list.
3. Choose to delete it and confirm.

Deletion is scoped to the exact record and document. The platform matches the document ID against that record's association type and ID, so you can only delete a file through the record it actually belongs to.

## What deletion removes

Deleting a document removes the document record and **all of its versions** from your company's database in one operation. Once deleted, the file no longer appears in the record's document list and can no longer be downloaded through the platform.

| Removed | Result |
| --- | --- |
| The document record | Gone from the record's document list. |
| Every version of the file | All version history is removed, not just the latest. |

> **Warning:** Deletion is permanent and covers every version. There is no undo and no recycle bin in the DocumentModule. If you only want to stop new uploads of a category rather than remove a specific file, deactivate the category instead.

## What you might see

| Situation | Result |
| --- | --- |
| The document does not exist for that record | "Document not found." (HTTP 404) |
| The record type is not supported | "Document operation unavailable" |
| No active company context | "Tenant context is required for document operations." |
| Successful deletion | The request returns no content; the list refreshes without the file. |

## Deleting versus replacing

If your goal is to correct a file rather than remove it, do not delete — upload a corrected copy instead. A new upload adds a new version and becomes the one that downloads return, while the earlier version stays in history. Delete only when the document should not exist at all.

> **Note:** Because documents are evidence for damage cases, penalty notices, and expense claims, confirm the file is genuinely unwanted before deleting. Once the versions are removed, the content cannot be recovered from the platform.

## Related

- Uploading a document and file size limits
- Downloading a document
- Document retention and permanent deletion
- Managing document categories

# Downloading a document

This article shows you how to find and download a file that was attached to a record in MobilityManager, and what the platform returns when you open one.

## Before you start

Downloading and previewing require the **documents:view** permission, typically held by FleetManager and SystemAdmin. You also need an active company (tenant) in your session; without one, document requests stop with "Tenant context is required for document operations."

## Find the document

1. Open the record the file is attached to — for example a vehicle, a Beleg (receipt/expense document), a damage case, or a Strafmandat (penalty notice).
2. Open that record's documents area. The platform lists the attachments for that record, newest first.

Each entry in the list shows the file name, content type, size, category, upload date, and who uploaded it. Use these details to pick the right file before downloading.

## Download or preview

Select the document you want. The platform streams the file back to you with its original file name and content type, so it opens or saves exactly as it was uploaded. If a file was uploaded without a detectable content type, it is served as a generic binary download.

> **Note:** Downloads always return the **latest version** of a document. If the file was replaced after its first upload, you receive the most recent version, not the original.

## What you might see instead of a file

| Situation | Result |
| --- | --- |
| The document ID does not exist for that record | "Document not found." |
| The stored file content is missing from storage | A not-found response indicating the content is unavailable. |
| You lack permission for that document's area | An access-denied response (HTTP 403) that does not reveal whether the file exists. |
| Storage could not be reached | A retrieval-failed message; try again shortly. |

The access-denied response is deliberately generic. It tells you that you cannot view the document without confirming or denying that a particular file is present, which prevents probing.

## Encrypted files download as normal files

When encryption at rest is enabled, files are decrypted on the way out automatically. You never handle keys or ciphertext — the download is the original, readable file. Files uploaded before encryption was enabled still download correctly, because the platform detects whether a stored payload is encrypted.

## Export artifacts

Some files are generated by the platform rather than uploaded by you — for example expense export spreadsheets. These export artifacts are retrieved by their document ID and are kept separate from the ordinary attachment lists, so you will not see them mixed in with a record's uploaded documents.

> **Tip:** If a colleague cannot open a file you can, check their role. Download is gated by **documents:view**, and some custom roles are granted upload rights without view rights or vice versa.

## Related

- Uploading a document and file size limits
- How documents work across the platform
- Deleting a document
- Tenant scoping and document isolation

# How documents work across the platform

MobilityManager stores every attached file through one shared component called the DocumentModule. This article explains how that module works so you understand the common rules that apply wherever you upload, view, or delete a file.

## One document engine for the whole platform

Instead of each area keeping its own file store, vehicles, damage cases, penalty notices, receipts, driver's licences, and compliance reports all route through the same DocumentModule. Whatever you upload is recorded the same way, encrypted the same way, and isolated to your company (tenant) the same way. The module exposes a small set of operations: upload, list metadata, preview/download, and delete.

## Associations: how a file knows where it belongs

A document is never stored on its own. Every file is attached to a parent record through two values:

- **Association type** — the kind of record. Supported types are `Vehicle`, `DamageCase`, `Beleg` (receipt/expense document), `Mandate` (Strafmandat / penalty notice), `driver-license`, and `license-compliance-report`.
- **Association ID** — the numeric ID of that specific record.

If you send an association type the module does not recognize, the upload is rejected with "Document operation unavailable". This is why you attach documents from inside a record (a vehicle, a Beleg, a damage case) rather than to a free-floating library.

## What the platform records for each document

| Field | Meaning |
| --- | --- |
| File name | Original file name, up to 256 characters. |
| Content type | MIME type, stored as sent by your browser. |
| File size | Size in bytes. |
| Category | A required system document category (for example invoice, photo, receipt). |
| Source type | `upload` for files you upload, or `url_ingestion` for files pulled from an external URL. |
| Uploader | Your user ID and email, kept for the audit trail. |

## Versioning

Each document keeps its file content as a numbered version. The first upload becomes version 1; uploading a replacement adds a new version rather than overwriting the previous one. Downloads always return the latest version, so history is preserved without cluttering the list.

## Permissions

Two permissions gate all document work: **documents:view** (list and download) and **documents:manage** (upload and delete). These are typically held by the FleetManager and SystemAdmin roles, and can be granted to custom roles. Permissions are enforced server-side via `[RequirePermission]`, so hiding a button is never the only line of defense.

> **Note:** Every document operation also requires a valid tenant context. If your session has no active company, the platform returns "Tenant context is required for document operations" and the action stops before any file is read or written.

## Related

- Uploading a document and file size limits
- Managing document categories
- Attaching documents to vehicles, damage cases, penalty notices, and receipts
- Encryption at rest and the encryption key

# Managing document categories

Document categories are the labels every uploaded file must carry, such as invoice, photo, or receipt. This article shows administrators how to create, edit, reorder, and retire categories, and explains the rules that keep them consistent.

## What categories are

Categories are defined once and shared across your whole platform — they are system-wide, not per company. Every upload must reference an **active** category, so the list you maintain here directly controls what users can choose when they attach files anywhere in the product.

## Who can manage categories

Viewing categories requires the **settings:view** permission; creating, editing, activating, deactivating, or deleting them requires **settings:manage**. These settings permissions are typically held by SystemAdmin. Custom roles can be granted them too.

## Category fields

| Field | Rules |
| --- | --- |
| Code | The stable identifier used when files are stored. Lowercase; only letters a–z, digits, hyphen, and underscore; 2–50 characters. Must be unique. It is normalized to lowercase automatically. |
| Display name | The human-readable label shown in the picker. Required, up to 120 characters. |
| Sort order | A number that controls list position. Lower numbers appear first; ties are broken alphabetically by display name. |
| Active | Whether the category can be selected on new uploads. |

## Create a category

1. Open the document categories settings.
2. Choose to add a category.
3. Enter a **code** (for example `invoice`) and a **display name** (for example "Invoice").
4. Optionally set a sort order and leave it active.
5. Save. If the code already exists or breaks the pattern rule, the platform rejects it with a specific message.

## Edit, reorder, or rename

You can update any field on an existing category. Changing the display name or sort order is safe at any time. Changing the code is possible but should be rare, because the code is what already-stored documents were saved under.

> **Warning:** Changing a category's code does not relabel documents that were uploaded under the old code. Prefer editing the display name and leaving the code stable.

## Deactivate instead of delete

To stop users from choosing a category on new uploads without disturbing existing files, deactivate it. Deactivated categories no longer appear as options, but documents already tagged with them keep their label and remain downloadable. You can reactivate a category later.

## Delete a category

Deleting removes the category definition entirely. Use this only for categories that were created in error and never used. For anything in active use, deactivation is the safer choice, because deletion removes the shared label from the system list.

> **Tip:** Keep the category list short and unambiguous. Because every upload across vehicles, damage cases, receipts, and penalty notices draws from the same list, a lean, well-named set makes uploads faster and reporting cleaner.

## Related

- How documents work across the platform
- Uploading a document and file size limits
- Supported file types
- Deleting a document

# Supported file types

This article explains which file types you can upload to MobilityManager, how the platform handles them, and why the category you choose matters more than the file format.

## The platform accepts any file type

The DocumentModule does not restrict uploads to a fixed set of file extensions or formats. Any file is accepted as long as it meets the two universal rules: it is not empty and it is within the **50 MB** size limit. What the platform enforces on every upload is a valid **category**, not a particular file format.

## How the file's type is recorded

When you upload, the platform stores the file's content type (MIME type) exactly as your browser reports it, along with the original file name and its extension. If no content type is provided, the file is recorded as a generic binary type. That stored content type is what the platform sends back on download, so files open in the right application later.

| Detail | Behavior |
| --- | --- |
| Content type | Stored as sent by the browser; up to 100 characters. Defaults to a generic binary type if absent. |
| File name | Preserved as uploaded, up to 256 characters. The extension is kept. |
| Size | Recorded in bytes; must be within the 50 MB limit. |

## Typical files by record type

Although any format is allowed, most teams use a small, predictable set:

- **Belege (receipts/expense documents)** — PDF or image scans of invoices and receipts.
- **Damage cases** — photos (JPEG, PNG) of the damage and PDF reports or estimates.
- **Strafmandate (penalty notices)** — PDF or image scans of the notice.
- **Driver's licence checks** — front and back images of the licence.
- **Vehicle files (Fahrzeugakte)** — registration documents, contracts, and inspection certificates, usually PDF.

> **Tip:** PDF is the safest choice for documents you want reviewers to open reliably across devices, and JPEG or PNG is best for photos. These are conventions, not restrictions — the platform will store other formats too.

## Practical guidance

Because there is no format filter, a few habits keep your document library clean and useful:

1. Prefer widely readable formats (PDF for documents, JPEG or PNG for photos) so colleagues can open files without special software.
2. Give files meaningful names before uploading — the name is preserved and shown in the document list.
3. Keep scans within 50 MB; if a multi-page scan is too large, lower the resolution or split it.

> **Note:** Since the platform does not inspect file contents for format, treat uploads like any file share: only upload files from trusted sources, and rely on your organization's own antivirus and endpoint controls for scanning.

## Related

- Uploading a document and file size limits
- Managing document categories
- How documents work across the platform
- Downloading a document

# Uploading a document and file size limits

This article shows you how to upload a file to a record in MobilityManager and explains the rules the platform enforces, including the 50 MB size limit and the mandatory category.

## Before you start

Uploading requires the **documents:manage** permission, typically held by FleetManager and SystemAdmin. You also need an active company (tenant) in your session, and the parent record you are attaching to must already exist.

## Upload a document

1. Open the record you want to attach the file to — for example a vehicle, a Beleg (receipt/expense document), a damage case, or a Strafmandat (penalty notice).
2. In that record's documents area, choose the file from your device.
3. Select a document **category**. This is required.
4. Confirm the upload. The platform stores the file, records its metadata, and saves the content as version 1.

Behind the scenes the file is sent to the shared DocumentModule with the record's association type and ID, so you do not choose those values manually — they come from the record you opened.

## The 50 MB size limit

Uploads are capped at **50 MB** per file. This limit is enforced framework-wide on the request body, so a file above the cap is rejected by the server before it is processed. If your file is too large, reduce it first — for example, scan at a lower resolution, or split a multi-page PDF.

> **Tip:** Photos straight from a phone camera are usually well under 50 MB, but high-resolution multi-page scans can exceed it. If an upload fails immediately with no category error, the file size is the most likely cause.

## What the platform validates

| Rule | What happens if it fails |
| --- | --- |
| A file is selected and not empty | "File is required." |
| A category is chosen | "Please select a document category" |
| The category is an active system category | "Category is required and must match an active system document category." |
| The record type is supported | "Document operation unavailable" |
| A company context is active | "Tenant context is required for document operations." |

Category values are normalized to lowercase before they are matched, so capitalization does not matter.

## Safe retries and duplicate prevention

If an upload hits a transient problem — a dropped connection, a timeout, or a temporary storage hiccup — the platform automatically retries with increasing back-off, up to five attempts. If all attempts fail, you see "Upload failed. Please retry." Validation problems (a missing category, a wrong record type) are not retried, because retrying cannot fix them.

The upload path also supports an idempotency key, which lets a repeated submission of the same request return the original result instead of creating a duplicate document. Your client handles this automatically; you do not need to set anything.

> **Note:** A successful upload creates a new version. Uploading again does not overwrite the earlier file — it adds version 2, version 3, and so on, and downloads return the newest.

## Related

- How documents work across the platform
- Supported file types
- Downloading a document
- Managing document categories

# Generating a compliance report

This article shows how to read the licence-verification compliance summary and generate an exportable compliance report for your fleet's driver's licence checks.

## Who can generate reports

Viewing the compliance summary and creating exports requires the `licenseVerification:report` permission, held by the FleetManager and SystemAdmin roles. Reporting is part of the licensed Führerscheinkontrolle module, so the module must be enabled for your tenant.

## Read the compliance summary

The report summary gives an at-a-glance view of verification coverage across your active drivers:

| Metric | Meaning |
| --- | --- |
| Total active drivers | Active drivers who have a licence profile. |
| Verified drivers | Drivers who have been verified at least once (they have a LastVerifiedAt). |
| Overdue drivers | Drivers whose NextVerificationDueDate has passed. |
| Coverage percent | Verified drivers as a share of total active drivers, rounded to one decimal place. |

> **Note:** The related compliance dashboard (available with `driverLicense:manage`) adds licence-class expiry detail — a compliance score, counts of expiring and expired classes, the overdue-verification count, and the date of the last verification cycle.

## Generate an export

1. Open the compliance reports area of the Führerscheinkontrolle module.
2. Choose the export **format** — `Pdf` or `Excel`. Any other value is rejected.
3. Set the report **scope** and, if you want to limit the range, a start and end date.
4. Create the export.

The platform creates an export job and processes it. The job moves through the statuses **Queued** → **Processing** → **Completed**, or **Failed** if an error occurs (in which case the job records error details). Generating an export is written to the module audit log with your user and email.

## Track your exports

Every export you create is kept in the export history, newest first. Each entry records its status, format, scope, the email of the requester, when it was created, and when it completed. You can open a specific export job by its identifier to check its current status and, once completed, retrieve the generated document reference.

> **Tip:** Use an Excel export when you need to sort or filter the underlying rows yourself, and a PDF export when you need a fixed record to file or share as evidence of compliance.

## If an export fails

A failed job stores a short error detail. Review it, adjust the scope or date range if needed, and create a new export. The original failed job remains in the history for traceability.

## Related

- Viewing a driver's verification history
- Scheduling recurring checks and setting the interval
- Reminders for upcoming and overdue checks
- Configuring the driver-licence module

# Viewing a driver's verification history

This article explains the verification history for a driver — the immutable audit trail of every licence check — and how to read and use it for compliance.

## What the verification history is

Every time a licence check is completed for a driver, the platform writes a permanent audit entry. The verification history is the full list of those entries for a single driver, showing each accepted or rejected check, who performed it, and when. Because entries are immutable, the history is a reliable record you can rely on during an audit.

## Who can view it

Viewing a driver's verification history requires the `driverLicense:manage` permission, held by the FleetManager and SystemAdmin roles. Drivers can see their own current verification status but not the full historical audit trail.

## Reading a history entry

Entries are listed newest first. Each one contains:

| Field | Meaning |
| --- | --- |
| Timestamp | When the check was recorded. |
| Outcome | **Accepted** or **Rejected**. |
| Verifier | The user who performed the check. |
| Verified licence classes | For accepted checks, the classes that were confirmed. |
| Rejection reason | For rejected checks, why it was rejected. |
| Front / back documents | References to the licence images reviewed, when the check came from an upload. |
| Verification request | The request the entry belongs to. |

## What produces an entry

- **Accepting an uploaded licence** — creates an Accepted entry with the confirmed classes and the reviewed document references.
- **Rejecting an uploaded licence** — creates a Rejected entry that records the reason.
- **Recording a manual check** — creates an Accepted entry with the confirmed classes and no documents, reflecting an in-person inspection.

> **Note:** Accepted checks (including manual checks) advance the driver's **LastVerifiedAt** and **NextVerificationDueDate**. Rejections are recorded in the history but do not move those dates, so the driver stays due until an accepted check is recorded.

## Using the history

- Confirm when a driver was last successfully verified and by whom.
- Review why past submissions were rejected before processing a new one.
- Provide an evidence trail during an internal or external compliance audit.

> **Tip:** Pair the per-driver history with the fleet-wide compliance report when preparing an audit — the report shows overall coverage and overdue counts, while the history proves the detail for any individual driver.

## Related

- Performing a verification: accept, reject, or manual check
- Generating a compliance report
- Uploading a driving licence for verification
- Scheduling recurring checks and setting the interval

# Configuring the driver-licence module

This article walks fleet managers and administrators through the driver-licence module settings — enforcement mode, expiry reminder thresholds, the reminder run time, and the verification cycle and reminder thresholds.

## Who can configure the module

Changing these settings requires the `driverLicense:configure` permission, held by the FleetManager and SystemAdmin roles. Settings are stored per tenant; if you have never saved them, sensible defaults apply.

## Settings reference

| Setting | Purpose | Default |
| --- | --- | --- |
| Enforcement mode | How booking enforcement reacts when a driver lacks the required licence class. | SoftWarning |
| Expiry reminder thresholds | Days-before-expiry at which licence-class and CPC reminders are sent. | 90, 60, 30, 14, 7 |
| Reminder execution time (UTC) | The time of day the reminder jobs are scheduled to run. | 06:00 |
| Verification cycle interval (days) | How long after a successful check the next check becomes due. | 180 |
| Verification reminder thresholds | Days-before-due at which drivers are reminded of an upcoming check. | 30, 14, 7 |

## Enforcement mode

Enforcement mode must be either **SoftWarning** or **HardBlock**.

- **SoftWarning** — bookings involving a licence mismatch are flagged but allowed to proceed.
- **HardBlock** — a licence mismatch prevents the booking.

## Expiry reminder thresholds

Provide between **1 and 10** threshold values. Each value must be between **1 and 365** days, and duplicates are not allowed. These thresholds drive the licence-class and CPC expiry reminders.

## Reminder execution time

Set the run time as a UTC time of day (for example `06:00`). This records when the daily reminder processing is intended to run for your tenant.

## Verification cycle and reminder thresholds

The verification cycle interval must be between **30 and 730** days. The verification reminder thresholds determine how far ahead drivers are warned about an upcoming check.

> **Warning:** Every verification reminder threshold must be smaller than the verification cycle interval. If any threshold is greater than or equal to the interval, saving fails with the message "Erinnerungsschwelle muss kleiner als das Kontrollintervall sein" (reminder threshold must be smaller than the check interval).

## Related reference-data configuration

The same `driverLicense:configure` permission also governs the reference data that supports licence checks:

- **Vehicle and model licence requirements** — the licence class required to drive a vehicle or every vehicle of a model, and whether CPC is required.
- **Country licence validity** — per-country validity years and renewal rules for each licence class.

> **Note:** The verification workflow itself (uploads, the queue, accept/reject, and reports) belongs to the licensed Führerscheinkontrolle module and only functions when the **EnableFuehrerscheinkontrolle** flag is enabled for your tenant. The settings above still apply to base licence data even when the module is off.

## Related

- Scheduling recurring checks and setting the interval
- Reminders for upcoming and overdue checks
- Who can upload versus who can verify licences
- Generating a compliance report

# Reminders for upcoming and overdue checks

This article explains the automatic reminders MobilityManager sends for upcoming and overdue driver's licence checks, and how licence-class and CPC expiry reminders work alongside them.

## Two reminder jobs run daily

The platform runs two background jobs once per day, each waking at **06:00 UTC** and processing every active driver:

- The **verification reminder job** — warns about upcoming and overdue licence checks (the Führerscheinkontrolle cycle).
- The **expiry reminder job** — warns about individual licence classes and CPC certificates that are approaching their expiry date.

> **Note:** Both jobs are restart-safe. They keep a per-driver send log, so a reminder is not sent twice on the same day even if the service restarts.

## Upcoming verification reminders

For each active driver with a **NextVerificationDueDate** in the future, the job checks how many days remain against the configured verification reminder thresholds (default **30, 14, and 7 days**). When the due date falls within a threshold, the driver receives a reminder. Only the single most relevant threshold is sent per day, so a driver is not flooded with duplicate notices.

## Overdue verification escalation

When a driver's **NextVerificationDueDate** has already passed, the job records an overdue reminder for that driver and adds them to a consolidated escalation for the fleet manager. Instead of one message per driver, the manager receives a single summary listing every overdue driver and how many days each is overdue.

## Licence-class and CPC expiry reminders

The expiry reminder job is separate from the verification cycle. It looks at the expiry dates you recorded on each licence class and at the CPC expiry date, and sends a reminder when the remaining days fall within an expiry threshold (default **90, 60, 30, 14, and 7 days**). As with verification reminders, only the most relevant threshold is sent per class per day, and already-expired items are handled separately from the advance warnings.

| Reminder type | Triggered by | Default thresholds (days) |
| --- | --- | --- |
| Verification due | NextVerificationDueDate approaching | 30, 14, 7 |
| Verification overdue | NextVerificationDueDate passed | Escalated to fleet manager |
| Licence-class expiry | A class's expiry date approaching | 90, 60, 30, 14, 7 |
| CPC expiry | CPC expiry date approaching | 90, 60, 30, 14, 7 |

## Adjusting the thresholds

Both sets of thresholds are configurable on the driver-licence settings page, which requires the `driverLicense:configure` permission. Verification reminder thresholds must each be smaller than the verification cycle interval, otherwise the settings are rejected.

> **Tip:** Keep accurate expiry dates on each licence class and the CPC fields — the expiry job relies entirely on those dates to decide when to warn.

## Related

- Scheduling recurring checks and setting the interval
- Configuring the driver-licence module
- Recording the licence classes a driver holds
- Generating a compliance report

# Scheduling recurring checks and setting the interval

This article explains how MobilityManager schedules recurring driver's licence checks, how the verification cycle interval works, and how to bring every active driver due for a check at once.

## How the recurring schedule works

Each driver-licence profile carries a **NextVerificationDueDate**. When a fleet manager accepts an uploaded licence or records a manual check, the platform sets **LastVerifiedAt** to the review time and calculates the next due date as that time plus the **verification cycle interval**. The driver then stays compliant until that date, when the next check becomes due.

## Setting the verification cycle interval

The interval is the **VerificationCycleIntervalDays** setting on the driver-licence settings page. Changing it requires the `driverLicense:configure` permission (FleetManager or SystemAdmin).

- The default interval is **180 days**, applied whenever no explicit interval is configured.
- Valid values range from **30 to 730 days**; values outside that range are rejected.

> **Note:** Changing the interval affects future calculations only. Drivers verified before the change keep the due date they were given at that time; their new interval applies from their next accepted check.

## Choosing an interval

A shorter interval (for example 90 days) means more frequent checks and stronger assurance but more administrative load; a longer interval (up to 730 days) reduces the workload. Pick the interval that matches your organisation's driver-safety policy.

## Bringing everyone due at once

You can force a verification cycle so that every active driver becomes due immediately. This is useful for an annual audit or when you first roll out the module. Triggering a cycle requires the `licenseVerification:verify` permission.

1. Trigger the verification cycle from the Führerscheinkontrolle module.
2. Every active driver's **NextVerificationDueDate** is set to today, marking them due.
3. Drivers who already have a Pending verification request are left untouched, so an in-progress review is not disrupted.

> **Warning:** Triggering a cycle makes all eligible active drivers overdue for their next check on the same day, which will generate reminders and populate the overdue lists. Use it deliberately.

## How the schedule connects to reminders and reports

- The daily reminder job reads **NextVerificationDueDate** to warn drivers before a check is due and to escalate overdue drivers to fleet managers.
- The compliance dashboard and report count drivers whose next due date has passed as overdue.

> **Tip:** Set the verification reminder thresholds to values smaller than the cycle interval so drivers receive advance warning within a single cycle. The settings page enforces this rule.

## Related

- Reminders for upcoming and overdue checks
- Configuring the driver-licence module
- Performing a verification: accept, reject, or manual check
- Generating a compliance report

# Performing a verification: accept, reject, or manual check

This article shows fleet managers how to review a driver's licence submission from the verification queue and complete the check by accepting it, rejecting it, or recording a manual (in-person) check.

## Who can verify

Reviewing and completing verifications requires the `licenseVerification:verify` permission, held by the FleetManager and SystemAdmin roles. The Führerscheinkontrolle module must be enabled for your tenant.

## Open the verification queue

The verification queue lists submitted requests. By default it shows **Pending** requests, but you can filter by status (or choose *All* to see every request). Requests are ordered with the longest-waiting at the top. Each queue item shows the driver's name and email, when it was uploaded, the licence classes on file, the current status, how many days it has been waiting, and the front and back document references to inspect.

## Accept a verification

Open the request, inspect the front and back document images, and compare them against the licence classes on the driver's profile.

1. Confirm the licence classes you can verify from the documents.
2. Accept the request, submitting the list of verified licence classes.

When you accept, the platform:

- Sets the request status to **Accepted** and records you as the reviewer and the review time.
- Writes an immutable audit entry with the outcome and the confirmed classes.
- Sets the driver's **LastVerifiedAt** to now and **NextVerificationDueDate** to now plus the verification cycle interval (180 days by default).

## Reject a verification

If the documents are unreadable, mismatched, or expired, reject the request. A **reason is required** — rejecting without one is refused. The reason is stored on the request and in the audit entry, and the driver can see it on their verification status so they know what to fix.

> **Note:** Rejecting does not change **LastVerifiedAt** or **NextVerificationDueDate**. The driver remains due until a check is accepted.

## Record a manual check

When you inspect a licence in person rather than by document upload, record a manual check against the driver. You must supply at least one verified licence class. This creates an already-accepted verification record with no documents, writes an audit entry, and refreshes the driver's verification dates exactly as an accepted upload does.

> **Warning:** If the driver already has a Pending request, a manual check is refused with a "pending request exists" conflict. Review the pending submission instead of creating a parallel record.

## Requests can only be reviewed once

Once a request is Accepted or Rejected, trying to accept or reject it again returns an "already reviewed" conflict. To re-check a driver, wait for or trigger a new request, or record a manual check.

## Related

- Uploading a driving licence for verification
- Viewing a driver's verification history
- Scheduling recurring checks and setting the interval
- Who can upload versus who can verify licences

# Recording the licence classes a driver holds

This article shows you how to record the licence classes a driver holds in their driver-licence profile (Fahrerakte), including issue and expiry dates, restriction codes, and professional-driver CPC details.

## Who can edit a licence profile

Creating or updating another driver's profile requires the `driverLicense:manage` permission, held by the FleetManager and SystemAdmin roles. Drivers can view their own profile (`driverLicense:view`) but cannot edit it or see colleagues' profiles.

## Open the driver's licence profile

From the Führerscheinkontrolle module, open the licence profile for the driver you want to edit. If the driver has no profile yet, saving licence details for the first time creates one.

## Enter the core licence details

Every profile needs a licence number; saving without one is rejected. Fill in the following:

| Field | Meaning |
| --- | --- |
| Licence number | Required. The number printed on the driving licence. |
| Issuing country | The country code that issued the licence. |
| Issue date | When the licence was first issued. |
| CPC issue / expiry date | For professional drivers, the Certificate of Professional Competence (Berufskraftfahrer-Qualifikation) dates. |
| Probationary / probation end date | Whether the driver is within the probation period (Probezeit) and when it ends. |

## Add the licence classes

Licence classes follow the EU class scheme (for example B, C, CE, D). You can look up the available classes and their meanings from the reference data, which includes each class's name, description, minimum age, a vehicle-type hint, and the lower classes it includes through the class hierarchy (for example CE includes C and B).

1. Add one row per class the driver holds.
2. Select the licence class code.
3. Enter that class's issue date and expiry date where applicable.
4. Enter any EU restriction codes (Schlüsselzahlen) that apply, such as the code requiring corrective lenses.

### Rules that apply when saving

- A profile can hold at most **16** licence classes.
- The same class code cannot appear twice; duplicate codes are rejected with a message naming the duplicate.
- Saving replaces the full set of classes: classes you remove from the form are deleted, and new rows are added.

> **Tip:** Enter an accurate expiry date on each class. The daily expiry-reminder job uses these dates to warn drivers and managers before a class lapses, and expired classes appear on the compliance dashboard.

## What you cannot edit here

The verification dates — **LastVerifiedAt** and **NextVerificationDueDate** — are not entered by hand. They are set by the verification workflow when a fleet manager accepts an uploaded licence or records a manual check.

> **Note:** Every profile update is written to the module audit log with the acting user, so changes to a driver's recorded classes are traceable.

## Related

- What the Führerscheinkontrolle is and why it is required
- Uploading a driving licence for verification
- Performing a verification: accept, reject, or manual check
- Viewing a driver's verification history

# Uploading a driving licence for verification

This article explains how to submit a driving licence for verification by uploading the front and back of the licence, and how to check the status of your submission afterwards.

## Who can upload

Uploading a licence for verification requires the `licenseVerification:upload` permission. Drivers hold this permission by default and can submit their own licence; FleetManager and SystemAdmin roles hold it too and can submit on a driver's behalf. Uploading is part of the licensed Führerscheinkontrolle module, so the module must be enabled for your tenant.

> **Note:** The driver must already have a driver-licence profile. If no profile exists, the upload is rejected with a "profile not found" error. Ask a fleet manager to record the licence classes first.

## Before you upload

The verification request references two documents that must already exist in the platform's document store: a front image and a back image of the driving licence. Prepare clear, legible scans or photos of both sides so the reviewing fleet manager can confirm the licence classes and validity.

## Submit the licence

1. Open the Führerscheinkontrolle module and go to your licence verification.
2. Provide the front-side document and the back-side document of the licence.
3. Submit the upload.

The platform creates a verification request with the status **Pending**, records who uploaded it and the upload time, and places it in the fleet manager's verification queue. The action is written to the audit log.

## Check your verification status

After uploading, you can view your current verification status. It shows:

- The most recent request and its status — **Pending**, **Accepted**, or **Rejected** (or **None** if you have never submitted one).
- **LastVerifiedAt** — when your licence was last confirmed.
- **NextVerificationDueDate** — when your next check is due.
- The rejection reason, if a previous submission was rejected.

## What happens next

A fleet manager reviews your pending request and either accepts it — confirming your licence classes and refreshing your verification dates — or rejects it with a reason. If your submission is rejected, read the reason, correct the issue (for example, upload a clearer image of the back of the licence), and submit again.

> **Tip:** While a request is Pending, a fleet manager cannot record a separate manual check for you — they are expected to review your pending submission instead. If you uploaded by mistake, ask your fleet manager to reject the request so a new one can be processed.

## Related

- Performing a verification: accept, reject, or manual check
- What the Führerscheinkontrolle is and why it is required
- Who can upload versus who can verify licences
- Reminders for upcoming and overdue checks

# What the Führerscheinkontrolle is and why it is required

This article explains what the Führerscheinkontrolle (driver's licence check) in MobilityManager is, why it is required, and how a check moves through the system from upload to a confirmed result.

## What the Führerscheinkontrolle is

The Führerscheinkontrolle is the process of confirming that a driver still holds a valid driving licence for the vehicles they operate. In MobilityManager it is a structured, repeatable check: a driver's licence is presented (as scanned documents or during a physical inspection), a fleet manager confirms the licence classes held, and the platform records the result and schedules the next check automatically.

Each driver has a driver-licence profile (Fahrerakte) that stores the licence number, issuing country, the licence classes held, any professional-driver CPC (Certificate of Professional Competence) dates, probation (Probezeit) information, and two verification dates: **LastVerifiedAt** (when the licence was last confirmed) and **NextVerificationDueDate** (when the next check is due).

## Why it is required

Employers who let staff drive company vehicles carry a duty of care to confirm that each driver is licensed for the vehicle they use. Letting an unlicensed or expired driver operate a vehicle exposes the organisation to liability. The Führerscheinkontrolle turns that duty into an auditable, recurring workflow so you can demonstrate, at any time, that every active driver was checked and when.

## How a check works end to end

1. A driving licence is uploaded for verification (front and back images), which creates a verification request with the status **Pending**.
2. A fleet manager reviews the request from the verification queue and either accepts it (confirming the licence classes) or rejects it with a reason.
3. On acceptance, the platform stamps **LastVerifiedAt** with the review time and sets **NextVerificationDueDate** to that time plus the configured verification cycle interval (180 days by default).
4. An immutable audit entry is written so the outcome, verifier, and confirmed classes are preserved for compliance.

A manager can also record a manual check (a physical, in-person inspection) without uploading documents. That produces the same confirmed result and updates the same dates.

## What the module tracks

- The licence classes a driver holds, each with its own issue date, expiry date, and any EU restriction codes.
- CPC issue and expiry dates for professional drivers.
- Probationary status and the probation end date.
- The verification status (None, Pending, Accepted, or Rejected) and the two verification dates that drive reminders and reports.

## Module licensing

The verification workflow is part of the licensed Führerscheinkontrolle module and is gated by the **EnableFuehrerscheinkontrolle** feature flag on your tenant's licence. When the module is disabled, verification endpoints return a `LICENSE_CAPABILITY_DISABLED` error. The base driver-licence data (profiles, licence classes, and the compliance dashboard) remains available even without the module.

> **Note:** Viewing your own licence profile requires the `driverLicense:view` permission, which drivers hold by default. Reviewing and confirming other drivers' licences requires the FleetManager or SystemAdmin role.

## Related

- Uploading a driving licence for verification
- Performing a verification: accept, reject, or manual check
- Who can upload versus who can verify licences
- Recording the licence classes a driver holds

# Who can upload versus who can verify licences

This article explains the difference between uploading a licence and verifying it, and which roles and permissions are required for each action in the Führerscheinkontrolle module.

## Two distinct responsibilities

The Führerscheinkontrolle deliberately separates the person who *submits* a licence from the person who *confirms* it. A driver (or someone acting for them) uploads the licence documents; a fleet manager reviews and decides the outcome. This separation keeps the audit trail meaningful — the verifier is a distinct, authorised reviewer.

## Permissions that control each action

| Permission | Allows |
| --- | --- |
| `driverLicense:view` | View your own licence profile and your own verification status; read licence-class reference data. |
| `driverLicense:manage` | Read and edit any driver's profile; view the compliance dashboard, another driver's verification status, and verification history. |
| `driverLicense:configure` | Change module settings, vehicle/model licence requirements, and country validity reference data. |
| `licenseVerification:upload` | Upload the front and back of a licence to create a verification request. |
| `licenseVerification:verify` | Review the queue; accept, reject, or record a manual check; trigger a verification cycle. |
| `licenseVerification:report` | View the compliance report summary and generate exports. |

## Default role assignments

- **Driver** — holds `driverLicense:view` and `licenseVerification:upload`. A driver can view their own profile and submit their own licence, but cannot verify anyone or see colleagues' profiles.
- **FleetManager** — holds all six driver-licence permissions, so a fleet manager can upload, verify, configure, and report.
- **SystemAdmin** — holds all six permissions as well.

> **Note:** These are the defaults for the built-in system roles. You can create custom roles and assign any subset of these permissions to match your organisation's separation-of-duties needs.

## Uploading versus verifying at a glance

- **Uploading** creates a Pending request and does not confirm anything on its own. Anyone with `licenseVerification:upload` can do it.
- **Verifying** (accept, reject, or manual check) is the decision step that updates the driver's verification dates and writes the audit entry. It requires `licenseVerification:verify`.

## Module licensing still applies

Holding the right permission is necessary but not sufficient: the verification actions live in the licensed Führerscheinkontrolle module. If the **EnableFuehrerscheinkontrolle** flag is off for your tenant, these endpoints return a `LICENSE_CAPABILITY_DISABLED` error regardless of permission. Base licence data (profiles and the dashboard) stays available.

## Related

- Uploading a driving licence for verification
- Performing a verification: accept, reject, or manual check
- Configuring the driver-licence module
- What the Führerscheinkontrolle is and why it is required

# Creating and editing a driver profile

This article explains how a driver profile (Fahrerprofil, the tenant-scoped driver record) comes into existence in MobilityManager and how to keep its details up to date. Driver profiles are not created from a blank form; they are provisioned from the underlying user account and the Driver role.

## What a driver profile is

A driver profile is a record inside your tenant database that represents one person who may book and drive vehicles. It is linked to a user account in the shared system database by a user ID, so one person has exactly one driver profile per tenant. Each profile holds:

| Field | Notes |
| --- | --- |
| First name / Last name | Mirrored from the user account; required |
| Email | Kept in sync with the user account |
| Phone | Optional |
| Licence number / Licence expiry date | Optional flat fields, superseded by the structured Führerscheinkontrolle (driver's licence check) |
| Active | Whether the driver takes part in pool and booking flows |

## Creating a driver profile

Because a profile is tied to a user, you create one by assigning the **Driver** role to an existing user account. This requires role-management permission (typically a SystemAdmin, or a FleetManager granted that permission).

1. Make sure the person has a user account in your tenant.
2. Assign the Driver role to that user.
3. The system automatically creates the matching driver profile in your tenant, copying the name and email from the user account.

> **Note:** There is no separate "add driver" form. If you assign the Driver role and a profile already exists, the existing profile is reused rather than duplicated.

## Editing profile details

The name and email on a driver profile are owned by the user account and are synchronised automatically. To change them, update the user's first name, last name, or email in user management; the driver profile picks up the new email on the next sync and backfills any missing name fields.

> **Tip:** For licence data, use the Führerscheinkontrolle (driver's licence check) profile rather than the flat licence-number field. The structured licence profile is the authoritative source and is what compliance checks rely on.

## What editing does not change

Editing the profile does not change which pools a driver can use. Pool access is managed separately through driver-to-pool assignment. Similarly, deactivating a driver is handled through the user account rather than by editing profile fields directly.

## Related

- How a driver profile is auto-created for a user
- Deactivating a driver
- Assigning drivers to a pool
- Viewing pool membership

# Deactivating a driver

This article explains how to take a driver out of service in MobilityManager and what happens to their pool access and booking ability when you do. Drivers are deactivated rather than deleted, so historical bookings and audit records stay intact.

## Deactivate, don't delete

A driver profile carries an active flag. When a profile is inactive, the record and its history remain in the tenant database, but the person is excluded from all live driver, pool, and booking operations. This preserves reporting and audit continuity while stopping the driver from taking new trips.

## How a driver becomes inactive

There is no standalone "deactivate driver" switch on the profile itself. A driver is taken out of service through the user account and role management:

1. **Deactivate the user account.** This prevents the person from signing in at all and is the most reliable way to stop bookings. Account deactivation requires user-management permission (typically SystemAdmin).
2. **Remove the Driver role** if the person should keep other access but no longer drive. This removes their driver capability going forward.

> **Warning:** Removing the Driver role stops new profiles from being provisioned, but an existing active profile can still be resolved for booking flows. To be certain a person can no longer book or drive, deactivate their user account.

## What an inactive driver can no longer do

Once a driver profile is inactive, the system excludes it everywhere it matters:

- It no longer appears in the tenant driver list used by fleet managers.
- It cannot be assigned to a pool — attempts are rejected with "Cannot assign inactive driver to pool".
- It is dropped from pool membership listings.
- It resolves to no accessible vehicles, so no vehicle is bookable.

## Reactivation

Driver deactivation is reversible. If you reactivate the user account, the profile's self-healing provisioning reactivates the matching driver profile automatically the next time it runs, restoring the person's shared-pool access. Any explicit pool assignments that were left in place also take effect again.

## Data-erasure deactivation

When a person exercises their right to erasure, the GDPR pseudonymisation process also deactivates their driver profile as part of scrubbing personal data: it clears the name, email, phone, and licence fields and sets the profile inactive. This is a permanent, one-way action and is separate from an ordinary reversible deactivation.

## Related

- How a driver profile is auto-created for a user
- Creating and editing a driver profile
- Removing a driver from a pool
- Viewing pool membership

# How a driver profile is auto-created for a user

This article describes how MobilityManager automatically provisions a driver profile (Fahrerprofil) for a user, so you understand where driver records come from and why one may appear without anyone creating it by hand.

## Why profiles are provisioned automatically

Booking and pool features need a tenant-scoped driver record, but the person's identity lives in the shared user account. To keep these in step, the system provisions a driver profile on demand instead of relying on someone to create it manually. This keeps booking flows working even if role assignment and tenant data drift apart.

## When provisioning happens

A driver profile is created (or repaired) at several points:

- **When the Driver role is assigned** to a user for your tenant, a matching profile is created immediately.
- **On first sign-in** for users who authenticate through single sign-on (OIDC), a profile is ensured as part of login.
- **Lazily, during booking-related actions**, if an active profile is still missing the system will create one on the spot.

## Conditions that must be met

Lazy provisioning only creates a profile when all of the following are true:

1. The user holds the **Driver** role for the current tenant.
2. The user account is active.
3. A tenant context can be resolved for the user.

If the user does not hold the Driver role, no profile is created and driver features remain unavailable to them.

## How the profile is filled in

The new profile copies details from the user account: first name, last name, and email. If the first name is blank, the email address is used as a fallback so the required name field is never empty. Names can also be derived by splitting the username when structured name fields are not available.

## Self-healing (repair) behaviour

When a profile already exists, provisioning also repairs it rather than duplicating it. On each run it may:

- Reactivate the profile if the user account is active again.
- Update the stored email to match the user account.
- Backfill a missing first name or last name.

> **Note:** There is only ever one profile per user per tenant. Repairs update that single record; they never create a second profile.

## What provisioning does not do

Auto-provisioning creates the driver record only. It does not grant access to any non-shared pool. Every driver automatically has access to the shared car-sharing pool, but access to other pools still requires an explicit driver-to-pool assignment.

## Related

- Creating and editing a driver profile
- Deactivating a driver
- The difference between shared pools and personal assignment
- Assigning drivers to a pool

# Assigning drivers to a pool

This article explains how to give a driver access to a non-shared pool in MobilityManager by creating a driver-to-pool assignment. Assignments are what let a driver book the vehicles in a restricted pool.

## Permission required

Assigning drivers to pools requires the vehicle-management permission, held by the **FleetManager** role and above. Drivers cannot assign themselves.

## When you need an assignment

You only need to assign a driver to **non-shared** pools. Every active driver already has access to the shared car-sharing pool automatically, so assigning someone to the shared pool is unnecessary. For any other pool, access exists only after you create an assignment.

## Assign a driver

1. Open the driver-to-pool assignment area.
2. Select the driver and the target pool.
3. Confirm the assignment.

The system records the assignment together with the date and time and the ID of the user who made it, so there is an audit trail of who granted access and when.

## Rules the system enforces

- The **driver must be active**. Assigning an inactive driver is rejected with "Cannot assign inactive driver to pool".
- The **pool must be active**. Assigning to an inactive pool is rejected.
- The **assignment must be new**. If the driver is already assigned to that pool, the request is refused as a conflict with "Driver is already assigned to this pool".

> **Note:** The "already assigned" conflict is harmless — it simply means the access you were adding is already in place.

## What the driver gains

Once assigned, the driver can access all active vehicles in that pool. Those vehicles appear in the driver's self-service list of bookable vehicles alongside the shared-pool vehicles they can already use. Access takes effect immediately.

> **Tip:** To grant a whole team access to a set of vehicles, create a dedicated non-shared pool, move those vehicles into it, and assign each team member to the pool. This keeps access easy to review and adjust.

## Related

- Removing a driver from a pool
- Viewing pool membership
- The difference between shared pools and personal assignment
- Restricting bookings to pool members

# Removing a driver from a pool

This article explains how to revoke a driver's access to a non-shared pool in MobilityManager by removing their driver-to-pool assignment, and what you can and cannot remove.

## Permission required

Removing a driver from a pool requires the vehicle-management permission, held by the **FleetManager** role and above.

## Remove an assignment

1. Open the driver-to-pool assignment area, or the pool's driver list.
2. Find the driver whose access you want to revoke.
3. Remove them from the pool and confirm.

This deletes the assignment record linking that driver to that pool. If the driver was not assigned to the pool in the first place, the action is rejected with "Driver is not assigned to this pool".

## What happens next

Access is evaluated live, so removal takes effect immediately. From that point the driver no longer sees the pool's vehicles in self-service and can no longer create new bookings for them. The driver's other pool access is unaffected — removing one assignment only revokes access to that single pool.

> **Note:** Removing a pool assignment does not deactivate the driver and does not change their access to any other pool, including the shared car-sharing pool.

## You cannot remove shared-pool access this way

The shared car-sharing pool grants access implicitly to every active driver, so there is no per-driver assignment record to remove. Trying to remove a driver from the shared pool has nothing to delete. To stop a driver reaching shared-pool vehicles, you have two options:

- Deactivate the driver, which excludes them from all pools; or
- Move the specific vehicles out of the shared pool into a non-shared pool and control access there with assignments.

## Existing bookings

Removing an assignment governs new bookings. Review any current or upcoming bookings the driver already holds for that pool's vehicles separately, since revoking pool access is about future access rather than cancelling trips already booked.

> **Tip:** Before removing access, use the pool's driver list to confirm the driver is explicitly assigned rather than an automatic shared-pool member. Only explicit assignments can be removed here.

## Related

- Assigning drivers to a pool
- Viewing pool membership
- Deactivating a driver
- The difference between shared pools and personal assignment

# Restricting bookings to pool members

This article explains how MobilityManager restricts vehicle bookings to drivers who have access to the vehicle's pool, and how you use pools and assignments to control exactly who can book what.

## How the restriction works

When a driver uses self-service booking, the system does not show them the whole fleet. It first works out which pools the driver can access, then offers only the active vehicles in those pools. A driver's accessible pools are:

- The **shared car-sharing pool**, available to every active driver automatically; plus
- Any **non-shared pool** the driver has been explicitly assigned to.

Vehicles that are not in any accessible pool never appear in the driver's list.

## Enforcement at booking time

The restriction is enforced when a booking is actually created, not just when the list is shown. If a driver attempts to book a vehicle whose pool they cannot access, the request is rejected with "You do not have access to this vehicle's pool". This means the rule holds even if a vehicle ID is submitted directly.

> **Note:** A vehicle must belong to a pool to be bookable at all. A vehicle with no pool is excluded from every driver's accessible list, so always assign new vehicles to a pool before expecting drivers to book them.

## Restricting a set of vehicles to specific drivers

1. Create a **non-shared** pool for the restricted vehicles.
2. Move those vehicles into the pool so they leave the shared pool.
3. Assign only the drivers who should be allowed to book them.

From that point, only the assigned drivers see and can book those vehicles.

## Opening vehicles to everyone

To make a vehicle available to the whole organisation, place it in the shared car-sharing pool. Because every active driver has automatic access to the shared pool, no per-driver assignment is needed.

## Changing access

Access is evaluated live, so changes take effect immediately:

- Assigning a driver to a pool lets them book that pool's vehicles right away.
- Removing a driver's assignment stops them booking that pool's vehicles from then on.
- Moving a vehicle to a different pool changes which drivers can book it.

> **Tip:** If a driver reports they cannot see a vehicle they expect, check three things: the vehicle is active and in a pool, the driver is active, and the driver is assigned to that pool (or the pool is the shared pool).

## Related

- Assigning drivers to a pool
- The difference between shared pools and personal assignment
- Adding vehicles to a pool
- Viewing pool membership

# Viewing pool membership

This article shows you how to review who has access to a pool and which pools a driver can use in MobilityManager. These views help you audit access and troubleshoot booking problems.

## Permission required

Viewing pool membership and driver assignments across the tenant requires the vehicle-management permission, held by the **FleetManager** role and above. Individual drivers can always see their own access through self-service.

## See the drivers in a pool

Open a pool's driver list to see everyone who can access it. How the list is built depends on the pool type:

- For the **shared car-sharing pool**, the list contains every active driver, each marked as automatic. There is no assignment date because access is implicit.
- For a **non-shared pool**, the list contains only explicitly assigned drivers, each shown with the date they were assigned.

> **Note:** The "automatic" indicator tells you whether a driver has access implicitly (shared pool) or through an explicit assignment. This is the quickest way to tell the two access models apart.

## See the pools a driver can use

You can also look up access from the driver's side. A driver's accessible pools combine the shared pool (always included) with any pools they have been assigned to. Each entry shows:

| Detail | Meaning |
| --- | --- |
| Pool name and description | Which pool the access applies to |
| Shared pool flag | Whether access is automatic |
| Common pool flag | Whether this pool is the tenant's designated common pool |
| Vehicle count | Number of active vehicles the driver could book from that pool |

A related view lists a driver's explicit assignments only, including when each was made and who made it, which is useful for audits.

## Review all drivers at once

A tenant-wide view lists all active drivers together with their pool assignments, so you can scan access across the whole organisation in one place. The tenant's designated common pool is indicated alongside.

## What drivers see themselves

Every driver has self-service views of their own access: the pools they can use and the specific vehicles available to them across all of those pools. Direct drivers here first when they are unsure what they are allowed to book.

## Related

- Assigning drivers to a pool
- Removing a driver from a pool
- The difference between shared pools and personal assignment
- Restricting bookings to pool members

# Adding vehicles to a pool

This article explains how to place a vehicle into a pool in MobilityManager and why pool membership matters for whether a vehicle can be booked. Assigning vehicles to pools is how you control access alongside driver-to-pool assignment.

## Permission required

Assigning a vehicle to a pool requires the vehicle-management permission, held by the **FleetManager** role and above.

## One pool per vehicle

Each vehicle belongs to at most one pool at a time. When you assign a vehicle to a pool, it is *moved* out of whatever pool it was in before. The action reports both the new pool and the previous pool so you can confirm the move.

## Add a vehicle to a pool

1. Open the target pool, or the vehicle you want to assign.
2. Choose to assign the vehicle to a pool and select the destination pool.
3. Confirm. The vehicle's pool is updated immediately and it leaves its previous pool.

> **Note:** You cannot assign a vehicle to an inactive pool. Reactivate the pool first, or choose an active one.

## Why pool membership matters

Pool membership determines who can book a vehicle:

- A vehicle in the **shared car-sharing pool** is bookable by every active driver.
- A vehicle in a **non-shared pool** is only bookable by drivers who are assigned to that pool.
- A vehicle with **no pool at all** does not appear in driver self-service and cannot be booked by drivers.

> **Warning:** If a newly added vehicle never becomes bookable, check that it has been assigned to a pool. Vehicles without a pool are silently excluded from the list of vehicles a driver can book.

## Vehicle counts

Pool listings show a vehicle count for each pool. This count includes only active vehicles. Decommissioned or otherwise inactive vehicles are not counted and are not offered for booking, even if they still reference the pool.

## Moving vehicles between pools

To reorganise your fleet, simply assign a vehicle to its new pool — there is no separate "remove from pool" step because the move handles it. To empty a pool before deleting it, move each of its vehicles to another pool.

## Related

- Creating a vehicle pool
- The difference between shared pools and personal assignment
- Restricting bookings to pool members
- Assigning drivers to a pool

# Creating a vehicle pool

This article shows you how to create and maintain a vehicle pool in MobilityManager. Pools are logical groupings of vehicles (for example "Executive Fleet" or "Depot North") that you use to control which drivers can book which vehicles.

## Before you start

Creating, renaming, and deleting pools requires the vehicle-management permission, which is granted to the **FleetManager** role and above. Drivers with view-only permission can see pools but cannot change them.

## Create a pool

1. Open the vehicle pools area for your tenant.
2. Choose to create a new pool.
3. Enter a **Name** (required, up to 100 characters). The name must be unique within your tenant; comparison ignores case, so "Depot" and "depot" are treated as the same.
4. Optionally add a **Description** (up to 500 characters) to explain the pool's purpose.
5. Save. The pool is created as an active, non-shared pool with no vehicles yet.

> **Note:** If you enter a name that already exists, creation is rejected with "A pool named '…' already exists". Choose a different name.

## The shared car-sharing pool

Every tenant already has one special pool, the "Shared Car-Sharing Pool", created automatically. It is flagged as the shared pool and every active driver has access to it without an assignment. You cannot create a second shared pool through the create action — new pools you make are always non-shared.

## Rename or update a pool

You can change a pool's name and description at any time with vehicle-management permission. The same uniqueness rule applies when you rename. The shared car-sharing pool is protected: attempts to rename it are rejected with "Cannot rename the shared car-sharing pool", although you can still update its description.

## Delete a pool

A pool can only be deleted when it is empty and is not the shared pool:

- The shared car-sharing pool can never be deleted.
- A pool that still contains active vehicles cannot be deleted — you will see a message stating how many vehicles remain.

To delete a non-empty pool, first move its vehicles to another pool, then delete it.

> **Tip:** If a pool is no longer needed but still holds history, consider leaving it in place rather than emptying and deleting it. Deletion is intended for genuinely unused pools.

## Related

- Adding vehicles to a pool
- The difference between shared pools and personal assignment
- Assigning drivers to a pool
- Viewing pool membership

# The difference between shared pools and personal assignment

This article compares the two ways a driver can gain access to a pool's vehicles in MobilityManager: automatic access through the shared pool, and explicit access through a personal driver-to-pool assignment. Understanding the difference helps you decide how to structure your fleet.

## Two access models

MobilityManager decides whether a driver can use a vehicle based on the vehicle's pool and the driver's access to that pool. Access is granted in one of two ways:

|  | Shared pool (automatic) | Personal assignment (explicit) |
| --- | --- | --- |
| Who can access | Every active driver | Only drivers assigned to that pool |
| How access is granted | Implicit — no action needed | A fleet manager assigns the driver to the pool |
| Applies to | The single "Shared Car-Sharing Pool" | Any non-shared pool |
| Membership record | None stored per driver | An assignment record with date and who assigned it |
| Shown in listings as | Automatic | Explicitly assigned |

## The shared pool

Each tenant has exactly one shared pool, created automatically and marked as the shared car-sharing pool. Every active driver can access its vehicles without any assignment, and an access check for the shared pool always succeeds for active drivers. Use the shared pool for vehicles you want to make freely available across the organisation.

## Personal (explicit) assignment

For any non-shared pool there is no automatic access. A driver can only book its vehicles after a fleet manager creates a driver-to-pool assignment linking that driver to that pool. This is how you restrict a group of vehicles to a specific set of drivers, such as reserving executive vehicles for eligible staff.

## Where the "common pool" fits in

A tenant can designate one pool as its **common pool** in tenant settings. This designation is a label used in the interface to highlight a default pool; it is marked as the common pool in driver and pool listings.

> **Note:** Being the common pool does not, on its own, grant automatic access. Unless the common pool is also the shared pool, drivers still need an explicit assignment to book its vehicles. Only the shared pool grants implicit access.

## Choosing an approach

- Put vehicles anyone may drive in the **shared pool**.
- Put restricted vehicles in a **non-shared pool** and assign only the drivers who should use them.

## Related

- Assigning drivers to a pool
- Restricting bookings to pool members
- Creating a vehicle pool
- Viewing pool membership

# Analyzing costs by cost center

Grouping the Fleet Cost Cockpit by cost center (Kostenstelle) lets you see fleet spend the way your finance team does — per department or accounting unit — and compare it against budget. This article covers how cost-center attribution works and how to read the budget-vs-actual view.

## Switch to the Cost center dimension

Set the dimension to **Cost center** and choose a period. The cockpit produces one bucket per cost-center code, each with its own total and category split, sorted by total cost with the highest first. This tells you which department or unit carries the most fleet cost.

## How costs reach a cost center

A cost record can carry its own cost center, or inherit one:

- **Receipts (Belege)** carry the cost center booked on the document.
- **Damage line items** and **leasing contracts** can specify a cost center directly.
- **Fuel** can carry a cost center from the refuelling record.
- **Fines** are attributed to the penalty notice's department allocation.
- When a cost has **no** cost center of its own but is tied to a vehicle, it inherits the **vehicle's cost center**. This is how maintenance and most fuel land in the correct Kostenstelle.

> **Note:** Costs that still can't be matched to a cost center appear under **(unassigned)**. A large (unassigned) bucket usually means some vehicles have no cost center set, or some fines have no department — fix the source data to move that spend into the right bucket.

## Budget vs actual by cost center

The cockpit compares each cost center's actual spend against an annual budget from the fleet budget table. The default budget view is by cost center for the current year. Each row shows:

| Column | Meaning |
| --- | --- |
| Budget | The cost center's planned amount for the year. |
| Actual | Costs collected for that cost center in the year. |
| Variance | Budget minus actual — positive is under budget, negative is over. |
| Utilization % | Actual ÷ budget × 100, to one decimal; blank when no budget is set. |

Cost centers that have a budget but no spend, and those with spend but no budget, both appear in the list so nothing is missed. The view also totals budget and actual across all rows.

> **Tip:** Budget vs actual is always annual. To watch a cost center trending toward its limit mid-year, read utilisation together with how far through the year you are — 60% utilisation in June is on track, but 60% in February is not.

## Managing the budgets

The budgets that drive this view are annual and can be scoped to a cost center, a specific vehicle, or the whole fleet. Creating or changing them requires the *fleetcost:budget:manage* permission, which is separate from viewing.

## Permissions

Viewing cost-center analysis and budget-vs-actual requires *fleetcost:view*; managing budgets requires *fleetcost:budget:manage*. Both are typically held by the FleetManager role and enforced by `[RequirePermission]`.

## Related

- Interpreting the cost KPIs
- Total cost of ownership per vehicle
- Exporting cost data to CSV
- How costs are aggregated across sources

# Calculating cost per kilometre

Cost per kilometre normalises a vehicle's total cost for how far it has been driven, so you can compare a high-mileage workhorse fairly against a rarely-used pool car. This article explains how to derive it from the figures the platform records.

## The two ingredients

Cost per kilometre combines a cost figure with a distance figure:

- **Cost** — a vehicle's Total-Cost-of-Ownership for the period, taken from the Fleet Cost Cockpit (or the Kosten tab of the Fahrzeugakte). This already includes fuel, maintenance, damage, insurance, leasing and receipts.
- **Distance** — the kilometres the vehicle covered in the same period, derived from its odometer. Each vehicle stores a current odometer reading (in kilometres); the distance for a period is the difference between the reading at the end and the start of that window.

Cost per kilometre is then simply the period TCO divided by the kilometres driven in that period.

> **Warning:** Use the same period for both figures. If you take a full-year TCO but only a partial-year odometer change, the result will be meaningless. Whole, matching windows are essential.

## RatePerKm is a different number

Each vehicle also has a configured **rate per kilometre** field (stored to four decimal places). This is the rate *charged* per kilometre driven — used for internal chargeback and mileage-based billing — and is set by hand on the vehicle. It is **not** the vehicle's actual cost per kilometre. Compare the two deliberately: if the configured rate you charge is well below the real cost per kilometre, mileage-based recovery is under-pricing that vehicle.

| Figure | Where it comes from | What it represents |
| --- | --- | --- |
| Actual cost per km | Period TCO ÷ kilometres driven | What the vehicle really costs to run per km |
| Rate per km (RatePerKm) | Configured on the vehicle | The chargeback rate applied to mileage |

## A worked approach

1. Pick a period — for example a full contract year.
2. Open the vehicle's TCO for that period in the cost tab or the Vehicle dimension of the cockpit.
3. Read the odometer at the start and end of the period and take the difference to get kilometres driven.
4. Divide the TCO by the kilometres to get cost per kilometre.

> **Tip:** Export the Vehicle-dimension cost view to CSV and add a distance column from your odometer records. A spreadsheet then gives you a cost-per-kilometre ranking across the whole fleet in one pass.

## Interpreting the result

- A high total but low cost per kilometre usually means a heavily-used but efficient vehicle — keep it.
- A modest total but high cost per kilometre points to an underused or costly-to-run vehicle — a candidate for pooling or replacement.
- Watch the mileage against the vehicle's allowed annual mileage: exceeding it can trigger leasing penalties that raise real cost per kilometre.

## Permissions

The cost figures require the *fleetcost:view* permission, usually held by the FleetManager role. Odometer readings are maintained on the vehicle record.

## Related

- Total cost of ownership per vehicle
- Comparing vehicles by cost
- Exporting cost data to CSV
- Interpreting the cost KPIs

# Total cost of ownership per vehicle

Every vehicle in MobilityManager has a Total-Cost-of-Ownership (TCO) figure that gathers all of its costs in one place. This article explains what that figure includes, where it appears, and how the period affects it.

## What per-vehicle TCO covers

A vehicle's TCO is the sum of every cost source the platform can attribute to that vehicle in a given period. It draws on exactly the same collectors as the fleet-wide Cost Cockpit, so a vehicle's total covers **all** registered sources — not just damage and receipts:

- Fuel from its refuelling records.
- Maintenance and service costs.
- Damage repair line items from its damage cases.
- Its share of insurance premiums.
- Leasing/financing, charged per active month.
- Receipts (Belege) booked against the vehicle, excluding drafts and rejected documents.

## Where you see it

The per-vehicle TCO powers the **Kosten (costs) tab of the Fahrzeugakte (vehicle file)** and is included in the vehicle file PDF export. The summary returns the vehicle's total for the period, a breakdown by cost category, and a month-by-month trend.

> **Note:** If a vehicle has no recorded costs in the period, the cost tab shows a clean zero state — a total of zero with empty category and trend lists — rather than an error. That means "nothing recorded yet", not "data missing".

## How the period is chosen

When no dates are supplied, the vehicle cost summary defaults to the current year to date: from 1 January of the current year up to now. Set an explicit "from" and "to" to report a specific window, such as a leasing contract year or a single quarter.

## Reading the category split

The categories are the same keys used across the cockpit: fuel, maintenance, damage, insurance, leasing, fines, and one entry per receipt type. They are ordered largest first, so you can see at a glance whether a vehicle's cost is dominated by fuel, financing or repairs.

> **Tip:** Use the monthly trend on the cost tab to separate a structural cost from a one-off. A steady monthly baseline is usually leasing plus routine fuel; a single tall month is typically a repair or an annual insurance installment.

## Attribution details worth knowing

- **Shared insurance** policies are split evenly across all vehicles they cover, so each vehicle carries its fair share rather than the whole premium.
- **Fines** are attributed to a cost center, not to a specific vehicle, so they do not appear in a single vehicle's TCO.
- **Cost centers** are inherited from the vehicle when a cost record doesn't specify one, keeping the vehicle's costs consistent with its Kostenstelle.

## Who can view it

The vehicle cost tab and its TCO figure rely on the fleet cost analytics and require the *fleetcost:view* permission, typically held by the FleetManager role. The Fleet Cost Cockpit module must also be licensed.

## Related

- Comparing vehicles by cost
- Calculating cost per kilometre
- How costs are aggregated across sources
- Interpreting the cost KPIs

# Comparing vehicles by cost

When you group the Fleet Cost Cockpit by the Vehicle dimension, you get a ranked, side-by-side comparison of what every vehicle costs. This article shows how to use that ranking to find your most and least expensive vehicles.

## Switch to the Vehicle dimension

Set the dimension to **Vehicle** and choose your period. The cockpit produces one bucket per vehicle, labelled by its licence plate, and returns them sorted by total cost with the most expensive vehicle first. Each bucket also carries its own category split (fuel, maintenance, damage, insurance, leasing, fines and receipts), so you can see not just *how much* a vehicle costs but *why*.

### What each vehicle total includes

A vehicle's total sums every cost source that can be attributed to it:

- Fuel from its refuelling records.
- Maintenance and service costs.
- Damage repair line items from its damage cases.
- Its share of insurance premiums (a shared policy is split evenly across the covered vehicles).
- Leasing/financing charged per active month.
- Receipts (Belege) booked against the vehicle.

> **Note:** Fines are attributed to a cost center rather than to an individual vehicle, so they are not part of a per-vehicle total. Compare vehicles on the costs that are genuinely vehicle-specific, and use the Cost center dimension when you need to include fines.

## Make the comparison fair

A raw total favours cheap answers — an old, barely-used vehicle will always look cheaper than a new, heavily-used one. Keep these points in mind:

- **Age in the period.** A vehicle added mid-year has fewer months of leasing and fuel than one present all year. Use equal, whole-month periods.
- **Utilisation.** A high total may simply reflect high mileage. Pair the comparison with cost per kilometre to normalise for distance driven.
- **One-off events.** A single large damage repair can push a vehicle to the top of the list for one period without indicating an ongoing problem — check its monthly trend.

## From ranking to root cause

Once the ranking highlights an outlier, drill into that vehicle's breakdown to see its category split and monthly trend. This tells you whether the cost is structural (high leasing or fuel every month) or a one-time spike (a repair or an annual premium).

> **Tip:** Export the Vehicle-dimension view to CSV to build your own comparison — for example dividing each vehicle's total by its recorded mileage in a spreadsheet, or flagging vehicles whose maintenance share is unusually high.

## Comparing pools instead

If you manage vehicles in pools, switch to the Pool dimension for the same ranked comparison at pool level. Because the cockpit inherits each cost's pool from its vehicle, pool totals aggregate all the vehicles assigned to that pool.

## Who can compare

Comparing vehicles requires the *fleetcost:view* permission, generally held by the FleetManager role. Exporting the comparison additionally requires *fleetcost:export*.

## Related

- Total cost of ownership per vehicle
- Calculating cost per kilometre
- Filtering the cost cockpit by dimension and period
- Exporting cost data to CSV

# Filtering the cost cockpit by dimension and period

The Fleet Cost Cockpit has two filters that shape every number on the screen: the dimension you group by and the period you report on. This article shows how each one changes the view.

## Filtering by dimension

The dimension controls how costs are grouped into buckets. Switching it does not change the grand total for a given period — it only changes how that total is broken down. The available dimensions are:

- **Fleet** — one combined total for the whole fleet. This is the default.
- **Vehicle** — one bucket per vehicle, labelled by licence plate.
- **Pool** — one bucket per vehicle pool.
- **Cost center** — one bucket per cost-center code (Kostenstelle).
- **Driver** — one bucket per driver.

Within each dimension, buckets are returned sorted by total cost, highest first, so the most expensive vehicle, pool or cost center is always at the top of the list.

> **Note:** Not every cost can be attributed to every dimension. Fines, for example, are not tied to a specific vehicle or driver, so under the Vehicle or Driver dimension they appear in the **(unassigned)** bucket. The grand total still includes them.

## Filtering by period

The period filter sets the "from" and "to" dates. Every collector only counts costs whose effective date falls inside that window:

- Fuel is filtered on the refuelling date.
- Belege are filtered on the Beleg date.
- Insurance and fines are filtered on the payment date.
- Maintenance is filtered on completion date (or scheduled/created date when no completion date exists).
- Damage is filtered on when each financial line item was recorded.
- Leasing charges one monthly rate for each month of the contract that overlaps the window.

### The default period

If you don't set dates, the cockpit reports the current year to date: from 1 January of the current year up to the present moment. Set a "from" date to start earlier, or a "to" date to close the window at a fixed point such as a month-end or quarter-end.

> **Tip:** To compare periods fairly, use equal-length windows — for example set 1 January to 31 March, then 1 April to 30 June. Because leasing is charged per active month, whole-month boundaries keep leasing costs comparable between periods.

## Combining the two filters

Dimension and period work together. A typical workflow is:

1. Set the period to the range you want to analyse.
2. Start on the Fleet dimension to read the grand total and category split.
3. Switch to Cost center or Vehicle to see which buckets drive that total.
4. Drill into a single bucket to see its category breakdown and monthly trend.

## Who can use the filters

Viewing the cockpit and its filters requires the *fleetcost:view* permission, usually held by the FleetManager role. The filters themselves are open to anyone who can view the cockpit — there is no separate permission for changing the dimension or period.

## Related

- What the Fleet Cost Cockpit shows
- Reading the cost trend over time
- Comparing vehicles by cost
- Analyzing costs by cost center

# How costs are aggregated across sources

This article explains where every euro in the Fleet Cost Cockpit comes from. Understanding how each source is collected helps you trust the totals and know what to fix if a number looks wrong.

## The seven cost sources

Each time you open the cockpit, it runs one collector per cost source over your selected period and normalises the results into a common cost record. The sources and their categories are:

| Category | Comes from | How it is dated and valued |
| --- | --- | --- |
| fuel | Refuelling records (Tankmanagement) | The record's total cost on its refuelling date. Records with no total cost are skipped. |
| maintenance | Maintenance/service records (Wartungsplanung) | The service cost, dated to completion date, else scheduled date, else the date it was created. |
| damage | Damage financial line items | The gross amount of each line item, dated to when it was recorded, attributed to the case's vehicle. |
| fines | Penalty-notice payments (Strafmandate) | Actual payments recorded against a notice, attributed to the notice's department cost center. |
| insurance | Paid insurance premium installments | The paid amount on its payment date, allocated to covered vehicles (see below). |
| leasing | Vehicle leasing/financing contracts | The monthly rate, charged once for each month the contract is active in the period. |
| belege:&lt;type&gt; | Belege (receipts/expense documents) | The gross total, on the Beleg date. Each receipt type is its own sub-category. |

## How dimensional keys are filled in

Some sources carry rich detail (Belege already know their vehicle, driver and cost center), while others carry less. To keep the picture complete, the cockpit enriches each record after collection:

- Any cost tied to a vehicle inherits that vehicle's **pool**.
- If a cost has no cost center of its own, it inherits the **cost center of its vehicle**. This is why maintenance and fuel appear under the right Kostenstelle even though those records don't always store one directly.
- Anything that still can't be matched to the selected dimension lands in the **(unassigned)** bucket rather than being dropped.

### Insurance allocation rule

A paid premium is allocated in this order: to the vehicles covered by the policy on the payment date; otherwise to the policy's single directly-linked vehicle; otherwise to the fleet with no vehicle key. When several vehicles are covered, the premium is split evenly between them.

### Leasing spread

A leasing contract's monthly rate is spread month by month across the period, charging the rate for every month the contract was active. This produces a smooth monthly trend instead of a single lump.

## What is excluded

> **Note:** Belege in *Draft* or *Rejected* status are excluded, because they do not represent committed cost. Only approved and posted receipts count toward the total.

> **Tip:** If one module is temporarily unavailable, the cockpit skips that single source and still shows the rest — so a total can be understated if a source failed to load. Re-open the view to pick up recovered sources.

## Related

- What the Fleet Cost Cockpit shows
- Interpreting the cost KPIs
- Analyzing costs by cost center
- Total cost of ownership per vehicle

# Interpreting the cost KPIs

The Fleet Cost Cockpit surfaces a small set of key figures. This article explains what each one means, how it is calculated, and how to read it without drawing the wrong conclusion.

## The grand total (TCO)

The headline figure is the grand total: the sum of every collected cost across all sources for the selected period and dimension. It is your Total-Cost-of-Ownership number for that window. Because it is a plain sum of committed costs, it moves only when real records are added — an approved Beleg, a paid installment, a recorded refuelling — so it is directly reconcilable against the underlying modules.

## Category totals

Below the grand total, costs are split by category and ordered largest first. The categories are fuel, maintenance, damage, fines, insurance, leasing, and one entry per receipt type (shown as `belege:<type>`). Reading the top two or three categories tells you immediately where your fleet spend concentrates.

> **Tip:** A category total that looks surprisingly low often means the source isn't being captured — for instance no leasing category usually means leasing contracts haven't been recorded, not that the fleet is unfinanced.

## Bucket totals

Each bucket (a vehicle, cost center, pool or driver) has its own total and category split, sorted so the most expensive bucket is first. This is the KPI you use to find outliers: the top bucket is where the money goes.

## Budget vs actual

For cost centers, vehicles, or the fleet as a whole, the cockpit can compare actual spend against an annual budget drawn from the fleet budget table. Each row reports:

| Figure | Meaning |
| --- | --- |
| Budget | The planned amount for the year, from the fleet budget. |
| Actual | Real costs collected for that item in the year. |
| Variance | Budget minus actual. Positive means under budget; negative means over. |
| Utilization % | Actual ÷ budget × 100, rounded to one decimal. Blank when no budget is set. |

> **Note:** Budgets exist for cost-center, vehicle and fleet scopes. Pools and drivers are not budgeted, so under those dimensions the budget shows as zero and utilisation is blank. A blank utilisation means "no budget on file", not "no spend".

## The (unassigned) bucket

Any cost that can't be attributed to the selected dimension is grouped under **(unassigned)**. Treat a large (unassigned) figure as a data-quality signal — for example fines with no department, or vehicles with no cost center — rather than a real cost centre. It still counts toward the grand total.

## Reading the KPIs safely

- Always check the **period** before comparing totals — the default is year to date, so a mid-year total is not a full-year figure.
- Over-budget utilisation early in the year may be normal if annual costs (like insurance) are paid up front.
- Pair absolute totals with cost per kilometre to avoid penalising high-mileage vehicles.

## Who can view the KPIs

All cockpit KPIs require the *fleetcost:view* permission. Editing the budgets that drive budget-vs-actual requires the separate *fleetcost:budget:manage* permission. Both are typically held by the FleetManager role.

## Related

- What the Fleet Cost Cockpit shows
- Analyzing costs by cost center
- How costs are aggregated across sources
- Reading the cost trend over time

# Reading the cost trend over time

The Fleet Cost Cockpit can show how costs move month by month, not just as a single total. This article explains where the monthly trend comes from and how to read it.

## Where the trend appears

The monthly trend is part of a **drill-down (breakdown)** into a single item in a dimension — one vehicle, one cost center, one pool, one driver, or the fleet as a whole. When you open the breakdown for a bucket, the cockpit returns, alongside the category split, a series of monthly points for the selected period. Each point is one calendar month with the total cost recorded in that month.

### How months are built

Every cost record carries an effective date (its refuelling date, Beleg date, payment date, completion date, and so on). The breakdown groups those records by year and month, sums the amounts in each month, and returns the points in chronological order. Months with no cost simply have no point.

> **Note:** Leasing is what makes the trend smooth. Because a leasing contract's monthly rate is charged once for each active month, a vehicle with a lease shows a steady baseline every month, with fuel, maintenance, damage and receipts adding variable spikes on top.

## Reading the trend

Use the shape of the line to answer questions the grand total can't:

- **Is spend rising or falling?** A steady climb across months points to growing running costs; a flat line dominated by leasing suggests a well-behaved, predictable vehicle.
- **Where are the spikes?** A single tall month usually maps to a one-off event — a large repair invoice (damage), a major service, or an annual insurance installment.
- **Are costs seasonal?** Fuel-heavy fleets often show winter peaks; comparing the same months across years highlights this.

## Getting a fleet-wide trend

To see the whole fleet's trend rather than a single vehicle, drill into the Fleet dimension. This aggregates every source into one monthly line, giving you the overall cost curve for the period. To isolate what is driving a spike, open the breakdown for the specific vehicle or cost center and read its category split for the same window.

> **Tip:** Widen the period to at least twelve months before reading the trend. A short window can make a normal annual insurance payment look like a runaway cost, when it is simply a once-a-year charge.

## Trend and category together

The breakdown always pairs the monthly trend with the category split for the same node. Read them together: the trend tells you *when* costs occurred, and the category totals tell you *what* they were. A spike in a given month plus a large "damage" category, for instance, tells you a repair drove that month.

## Who can view trends

Drill-downs are part of the cockpit and require the *fleetcost:view* permission, typically held by the FleetManager role.

## Related

- Filtering the cost cockpit by dimension and period
- Interpreting the cost KPIs
- Total cost of ownership per vehicle
- How costs are aggregated across sources

# What the Fleet Cost Cockpit shows

The Fleet Cost Cockpit gives you a single, consolidated view of what your fleet actually costs. It rolls up every recorded cost source into a Total-Cost-of-Ownership (TCO) figure and lets you slice that total across five dimensions and any date range.

## What the cockpit consolidates

Instead of opening the fuel, maintenance, damage, insurance, leasing, penalty-notice and Belege (receipt/expense document) modules one at a time, the cockpit collects committed costs from all of them and presents them together. For the period you choose it shows you three things:

- **A grand total** — the sum of all costs in the selected period and dimension.
- **Category totals** — how that total splits across fuel, maintenance, damage, fines, insurance, leasing and receipt categories, ordered from largest to smallest.
- **Buckets** — one row per item in the dimension you selected (for example one row per vehicle or one row per cost center), each with its own total and category split, sorted by cost descending.

## The five dimensions

You choose how costs are grouped by picking a dimension. The cockpit supports:

| Dimension | Groups costs by |
| --- | --- |
| Fleet | The whole fleet as one total (the default view) |
| Vehicle | Each individual vehicle, labelled by licence plate |
| Pool | Each vehicle pool |
| Cost center | Each cost-center code (Kostenstelle) |
| Driver | Each driver |

Costs that cannot be tied to the chosen dimension — for example a fine that is not linked to a specific vehicle — are grouped into an **(unassigned)** bucket so nothing is silently dropped from the total.

## Default period

When you open the cockpit without setting dates, it shows the current year to date: from 1 January of the current year up to now. You can narrow or widen this with the period filter at any time.

> **Note:** The cockpit reads from live cost records. As soon as a refuelling, an approved Beleg, a paid insurance installment or a maintenance cost is recorded, it flows into the next cockpit view — there is no separate import step.

## Who can see it

Access is gated by the *fleetcost:view* permission ("View fleet cost cockpit and TCO analytics"), enforced through `[RequirePermission]`. This is typically held by the FleetManager role. The module also has to be enabled in your licence; if the Fleet Cost Cockpit entitlement is not licensed, the screen is not reachable.

> **Tip:** Start at the Fleet dimension to see the headline TCO and the biggest cost categories, then switch to Vehicle or Cost center to find where the money is going.

## Related

- How costs are aggregated across sources
- Interpreting the cost KPIs
- Filtering the cost cockpit by dimension and period
- Total cost of ownership per vehicle

# Exporting cost data to CSV

The Fleet Cost Cockpit can export the current cost summary as a CSV file so you can analyse it in a spreadsheet, share it with finance, or archive it. This article explains exactly what the export contains and how to use it.

## What the export produces

The export renders the cost summary for the dimension and period you have selected into a single CSV file. It contains one row for every bucket-and-category combination, followed by a total row. The columns are:

| Column | Contents |
| --- | --- |
| Dimension | The dimension the summary was grouped by (fleet, vehicle, pool, costcenter or driver). |
| BucketKey | The stable key of the bucket — e.g. a vehicle id, a cost-center code, or `unassigned`. |
| BucketLabel | The human-readable label — e.g. a licence plate, cost-center name or driver name. |
| Category | The cost category: fuel, maintenance, damage, fines, insurance, leasing, or `belege:<type>`. |
| Amount | The amount for that bucket and category. |

The final line is a grand-total row in the form `TOTAL,,,,<amount>`, giving the sum across every bucket and category so the file reconciles to the cockpit's headline figure.

## The file name

The download is named for the dimension and the export date, for example `fleet-cost-costcenter-20260706.csv`. Because the date stamp is part of the name, repeated exports don't overwrite each other and stay easy to sort chronologically.

> **Note:** The export mirrors the summary, so it always reflects the same dimension and period filters that are active on screen. Set your dimension and date range first, then export.

## How to export

1. Open the Fleet Cost Cockpit and choose the dimension you want (for example Cost center or Vehicle).
2. Set the period. If you leave the dates unset, the export covers the current year to date, matching the default cockpit view.
3. Trigger the export. The file downloads as a CSV.

## Working with the file

- Because each row is one bucket-and-category pair, a pivot table over BucketLabel and Category rebuilds the on-screen breakdown instantly.
- The TOTAL row is a footer, not a data row — exclude it before pivoting or summing to avoid double-counting.
- Amounts use the platform's standard export number formatting; check your spreadsheet's locale on import if figures look off.

> **Tip:** To build a cost-per-kilometre report, export the Vehicle dimension and add a distance column from your odometer readings in the spreadsheet. The BucketLabel (licence plate) lets you match rows to vehicles.

## Who can export

Exporting requires the dedicated *fleetcost:export* permission ("Export fleet cost reports"), which is separate from the *fleetcost:view* permission used to see the cockpit. A user can therefore be allowed to view costs on screen but not to download them. Both are typically granted to the FleetManager role and are enforced through `[RequirePermission]`.

> **Warning:** Exported cost files contain detailed financial data across cost centers, vehicles and drivers. Grant the export permission deliberately and handle downloaded files in line with your data-protection policy.

## Related

- What the Fleet Cost Cockpit shows
- Analyzing costs by cost center
- Comparing vehicles by cost
- Interpreting the cost KPIs

# Fuel cost overview and reporting

The fuel module gives you a live cost overview on its dashboard plus three reports and CSV exports for deeper analysis. This article explains what each one shows and how to read it. Everything here needs the **tankmanagement:view** permission, held by FleetManager and SystemAdmin by default.

## The dashboard overview

The dashboard is the at-a-glance view of your fuel operation. It shows:

| Metric | What it counts |
| --- | --- |
| Total tanks | All tanks on record. |
| Active tanks | Tanks currently active. |
| Low-level alerts | Active tanks below their minimum-level threshold. |
| Monthly spend | Total fuel cost for refuellings dated in the current calendar month. |
| Budget utilisation | Average utilisation of the active budgets covering today. |
| Recent refuellings | Number of refuellings in the last 30 days. |

## Cost-analysis report

The cost-analysis report groups spend by tank and cost center over a period (the last three months if you leave the dates blank). Only refuellings that carry a total cost are included. For each group it shows total cost, total litres, average cost per litre, and the refuelling count, ordered by total cost, with a grand total across all groups. Use it to see where fuel money is going and to compare unit prices between cost centers.

## Budget-versus-actual report

This report compares each active fuel budget against what was actually spent. A budget is a spend cap for a period, optionally scoped to a cost center and a fuel type. Actual spend is computed on demand from the refuelling records that fall inside the budget's period and match its cost center and fuel type. For each budget you see:

- **Budget amount** — the cap.
- **Actual spend** — matched refuelling cost in the period.
- **Variance** — budget minus actual; negative means overspend.
- **Utilisation %** — actual as a percentage of the cap.

Budgets are ordered by utilisation, highest first, so the ones closest to or over their cap are at the top. Creating and editing budgets needs the **tankmanagement:manage** permission.

> **Note:** A fuel-type-scoped budget only matches refuellings drawn from a tank of that fuel type, because the fuel type lives on the tank. External refuellings with no tank will not count toward a fuel-type budget.

## Consumption report

The consumption report covers volume rather than money — total litres, refuelling count, and average litres per refuelling per tank. It is covered in detail in its own article.

## CSV exports

Three exports let you take fuel data into a spreadsheet:

- **Refuelling history** — every refuelling in an optional date range, including tank, vehicle, driver, litres, cost per litre, total cost, odometer, receipt number, fuel card, cost center, and notes.
- **Fuel cards** — all cards with provider, assignment, monthly limit, status, and expiry.
- **Tanks** — all tanks with fuel type, capacity, current and minimum level, fill percentage, and low-level flag.

> **Tip:** The refuelling-history export is the backbone of month-end reconciliation — filter to the period, export, and pivot by vehicle or cost center in your spreadsheet to tie fuel spend back to the linked expense documents.

## Related

- Track fuel consumption per vehicle and tank
- Spot anomalies and plausibility checks
- Assign a refuelling to a vehicle, driver, or cost center
- What fuel management (Tankmanagement) does

# Spot anomalies and plausibility checks

Bad fuel data usually shows up as an implausible price, an out-of-range quantity, or spend that overshoots a budget. This article explains the plausibility checks the module applies at capture and how to hunt down anomalies afterwards using the reports and exports. You need **tankmanagement:view** to read the reports and **tankmanagement:refuel** or **tankmanagement:manage** to enter data.

## Plausibility checks at capture

The module refuses obviously wrong values when a refuelling is saved, so the first line of defence is automatic:

- Quantity must be greater than zero.
- Cost per litre, total cost, and odometer reading cannot be negative.
- The refuelling date is required and cannot be more than one day in the future.
- If you supply only one of cost per litre or total cost, the other is derived, so the two can never contradict each other.

Imported fuel-card rows go through the same idea: rows that fail validation are flagged as *Error* and are excluded from commit, and rows with no vehicle match are flagged *Unmatched* for you to resolve.

## Spotting cost anomalies

The **cost-analysis report** is your best tool for outlier prices. It groups spend by tank and cost center and, for each group, shows total cost, total litres, and the *average cost per litre*. A group whose average cost per litre is far from your others points to a mistyped price or a wrong quantity.

1. Open the cost-analysis report and set the period.
2. Scan the average-cost-per-litre column for values that stand out.
3. Cross-check the current fuel price for that fuel type as a sanity reference.

## Spotting consumption anomalies

The **consumption report** shows average litres per refuelling per tank. A tank whose average jumps suggests over-dispensing, a leak, or fills recorded against the wrong tank. Combine this with per-vehicle odometer readings to find a vehicle whose litres-per-100-km has drifted.

## Budget and level warnings

- **Budget-versus-actual** surfaces overspend directly: a negative variance or a utilisation above 100% means actual fuel spend has passed the cap for that period, cost center, and fuel type.
- The **dashboard** shows low-level alerts — active tanks whose current level has fallen below their minimum threshold — which catches a tank that has been drawn down faster than expected.

> **Tip:** Export the refuelling history to CSV and sort by cost per litre or by quantity. Extreme highs and lows at the top and bottom of the sort are the quickest way to eyeball data-entry mistakes across a whole period.

> **Note:** The module does not run an automated anomaly-detection engine. Plausibility is enforced at capture, and everything beyond that is review-driven — use the cost-analysis, consumption, and budget-versus-actual reports plus the CSV export to investigate.

## When you find a bad record

Correct the cost on the linked Beleg (receipt/expense document), fix a tank's level with a tank reading, and keep cost-center codes consistent so groups do not fragment. See the article on correcting a refuelling record for the full procedure.

## Related

- Correct a refuelling record
- Fuel cost overview and reporting
- Track fuel consumption per vehicle and tank
- Import fuel-card transactions

# Track fuel consumption per vehicle and tank

The consumption report tells you how much fuel each tank has drawn over a period, and the refuelling history with odometer readings lets you work out a single vehicle's economy. This article covers both. You need the **tankmanagement:view** permission to open the report.

## The consumption report

The consumption report aggregates refuelling records over a period and groups them by tank. For each group it shows:

| Column | Meaning |
| --- | --- |
| Tank | The tank the fuel was drawn from (external fills group together with no tank). |
| Fuel type | The tank's fuel type key. |
| Total litres | Sum of litres across all fills in the group. |
| Refuelling count | Number of fills in the period. |
| Average litres per refuelling | Total litres divided by the count. |

Groups are ordered by total litres, largest first, and the report shows a grand total across all groups.

### Choosing the period

1. Open the consumption report.
2. Set a period start and end. If you leave them blank, the report covers the last three months up to today.
3. Read the per-tank totals and the grand total.

> **Note:** The consumption report groups by tank, not by vehicle. External refuellings that are not tied to a tank — such as public-station fills on a fuel card — are aggregated into a single tank-less group.

## Working out per-vehicle consumption

To see one vehicle's fuel use, work from the refuelling history rather than the consumption report. Each refuelling can carry a vehicle and an odometer reading, so a vehicle's fills over time give you both the litres burned and the distance covered.

1. Open the refuelling history and filter by the vehicle (and, if you like, a date range).
2. Note the litres and odometer reading on each fill.
3. Take the distance between two consecutive odometer readings and the litres added between them to compute economy — litres per 100 km is *litres ÷ distance × 100*.

For a clean calculation, encourage drivers to record the odometer on every fill and to fill to full each time, so the litres added map to the distance since the last full tank.

> **Tip:** Export the filtered refuelling history to CSV to run the per-vehicle maths in a spreadsheet. The export includes the vehicle, litres, odometer reading, and date for every record in the range.

## Keeping the numbers trustworthy

- Consistent odometer readings are essential — a skipped or mistyped reading breaks the distance between fills.
- Attribute every fill to the correct vehicle so nothing is missed from its history.
- Compare the average litres per refuelling across tanks to spot a tank that is being over-drawn.

## Related

- Fuel cost overview and reporting
- Spot anomalies and plausibility checks
- Record a refuelling entry
- Assign a refuelling to a vehicle, driver, or cost center

# Assign a refuelling to a vehicle, driver, or cost center

Assigning each refuelling to the right vehicle, driver, cost center, and fuel card is what makes fuel reporting meaningful. This article explains the assignment fields on a refuelling record and how they feed reporting. You need the **tankmanagement:refuel** permission to set them when recording a fill-up.

## The four ways to attribute a fill-up

| Field | What it links to | Why it matters |
| --- | --- | --- |
| Vehicle | A vehicle in your fleet | Groups fuel by vehicle and, with the odometer, enables per-vehicle consumption. |
| Driver name | Free-text driver name | Records who fuelled; carried on the refuelling record. |
| Cost center (Kostenstelle) | A cost-center code | Drives the cost-analysis report and budget matching. |
| Fuel card | A provider fuel card | Ties the fill-up to a card so card spend can be reconciled. |

## Assigning during entry

1. Record the refuelling as usual, entering litres, date, and price.
2. Select the **vehicle** the fuel went into.
3. Enter the **driver name** if you track it.
4. Enter the **cost center** code to attribute the spend.
5. Select the **fuel card** if the fill-up was paid on one.
6. Save. The assignment is stored on the record and, if there is cost data, copied onto the linked Beleg (receipt/expense document).

> **Note:** Driver is captured as a free-text name on the refuelling record, not a link to a driver account. Use it consistently so history stays searchable — the refuelling list can be searched by driver name, receipt number, and notes.

## How assignment flows into reporting

- **Cost-analysis report** groups spend by tank and cost center, so a missing cost center leaves that spend in an unattributed group.
- **Budget-versus-actual** matches refuellings to a budget by cost center and, for fuel-type-scoped budgets, by the tank's fuel type. A refuelling with no cost center will not count toward a cost-center budget.
- **Vehicle** and **fuel card** are both filters on the refuelling list and on the CSV export, so you can pull one vehicle's or one card's history.
- The linked expense document carries the vehicle and mileage into the Fahrzeugakte (vehicle file) cost view.

## Filtering by assignment

On the refuelling history you can filter by vehicle, by fuel card, and by date range, then export the result to CSV. This is the quickest way to check that a vehicle's or card's fills are all attributed correctly before you run month-end reports.

> **Tip:** Decide on a cost-center convention before drivers start recording fuel. Because budgets and the cost-analysis report key off the cost-center code exactly, an inconsistent code (for example a typo or a trailing space) creates a separate group and understates a budget's utilisation.

## Related

- Record a refuelling entry
- Import fuel-card transactions
- Fuel cost overview and reporting
- Track fuel consumption per vehicle and tank

# Correct a refuelling record

Because a refuelling entry feeds both your tank levels and your expense records, correcting one takes a moment's care. This article explains what is validated at capture, where cost corrections actually happen, and how to fix a tank level that a refuelling put out of step.

## Get it right at capture

The module blocks the most common mistakes when you save a refuelling, so many "corrections" are avoided up front. It rejects a quantity that is zero or negative, a negative cost or odometer, a missing date, and a date more than one day in the future. If you enter only one of cost-per-litre or total cost, the other is derived automatically, which prevents the two from disagreeing.

> **Note:** A refuelling record is written as an audit-logged event. Each creation is stamped with the acting user and a trace ID, so the history of who recorded what is preserved.

## Correcting the cost

When a refuelling includes a price, the cost lives on the linked Beleg (receipt/expense document) that the module creates automatically. That linked document — not the fuel entry — is where financial corrections belong:

- While the Beleg is still a draft or has been returned for clarification, its line items, amounts, VAT rate, and cost center can be edited in the Belege module.
- During review, a financial correction can be applied to the Beleg's tags before it is approved.
- Once the Beleg is approved, its financial data is snapshotted and frozen for audit, so corrections must be made before approval.

Working through the linked expense document keeps the cost you report and the cost your finance team approves in one consistent place.

## Correcting a tank level

A refuelling dispensed from a company tank reduces that tank's current level by the litres recorded. If a fill-up was logged against the wrong tank or with the wrong quantity, the tank's on-hand level will be off. To bring it back in line, record a tank level reading with the true measured level. This needs the **tankmanagement:refuel** permission.

1. Open the affected tank.
2. Record a new level reading with the actual measured litres and the reading date.
3. Add a note explaining the adjustment for the audit trail.

## Correcting attribution

If the litres and cost are right but the fill-up is attributed to the wrong vehicle, cost center, or fuel card, correct the attribution on the linked expense document where it drives reporting. Keep your cost-center codes consistent — the cost-analysis report and budgets match on the exact code, so a mistyped code silently splits a group.

> **Warning:** The fuel module does not expose an in-place "edit" of a posted refuelling entry. Treat each entry as a ledger line: correct the linked Beleg for money and attribution, and use a tank reading to fix a tank level. For a structural data error you cannot resolve this way, ask a SystemAdmin.

## Related

- Record a refuelling entry
- Assign a refuelling to a vehicle, driver, or cost center
- Spot anomalies and plausibility checks
- Fuel cost overview and reporting

# Import fuel-card transactions

Instead of keying fuel-card transactions one by one, you can import a provider file and let the module match each transaction to a vehicle and turn the batch into draft expense documents. This article walks through the upload, review, and commit workflow. Every step needs at least **tankmanagement:view**; the upload, matching, and commit actions need **tankmanagement:manage**.

## Before you import

1. **Register a provider.** Create a fuel-card provider record with a name and a provider key. The key must match a parser the system ships. Available parsers are **Generic CSV** (`generic-csv`), **DKV** (`dkv`), and **UTA** (`uta`), all of which read `.csv` files.
2. **Optionally add card mappings.** Map a card number to a vehicle, with an optional validity window, so transactions on that card match automatically even when the file has no plate or VIN.

## Step 1 — Dry run (upload and preview)

Upload the provider file together with the provider and an idempotency key. The module parses the file, validates each row, and matches rows to vehicles, then stores everything as a staging import job in *Pending* status — nothing is committed yet. You get a preview summary:

| Counter | Meaning |
| --- | --- |
| Total rows | Transactions parsed from the file. |
| Valid rows | Rows that passed validation. |
| Matched | Rows linked to a vehicle. |
| Unmatched | Valid rows with no vehicle yet. |
| Error rows | Rows that failed validation and cannot be committed. |

> **Note:** The idempotency key prevents duplicate imports. Re-uploading the same file with the same key returns the existing job instead of creating a second one.

### How rows are matched to vehicles

Matching tries three methods in order and stops at the first hit, against active vehicles only:

1. **VIN** — the row's VIN against the vehicle VIN.
2. **Licence plate** — the plate, normalised (spaces and dashes removed).
3. **Card mapping** — the card number against a mapping valid on the transaction date.

Rows with no hit are marked *Unmatched*; rows that fail validation are marked *Error*.

## Step 2 — Review and fix matches

Open the job to see every row with its match status, match method (VIN, LicensePlate, CardMapping, or Manual), and any validation messages. For each unmatched row you can assign a vehicle manually; the row then becomes *Matched* with method *Manual*, and the job's counters update.

## Step 3 — Commit

Committing processes only valid, non-error rows. Rows are grouped by vehicle and invoice number, and each group becomes one draft Beleg (receipt/expense document) of type *fuel*, with a line item per transaction (fuel type, station, quantity, unit price, and derived VAT rate). The job moves to *Committed* and reports how many expense documents were created.

> **Warning:** Only a job in Pending status can be committed, and committing is one-way for that job. Re-running commit on an already-committed job simply returns the counts from the first commit; it does not create the documents again.

## Generic CSV format

For the Generic CSV parser the required columns are `transactionDate`, `fuelType`, `quantity`, and `grossAmount` (gross must be greater than zero). Optional columns include `cardNumber`, `licensePlate`, `vin`, `station`, `unitPrice`, `netAmount`, `vatAmount`, `odometer`, `currency`, and `invoiceNumber`. The free-text fuel type is resolved to a fuel-type key automatically.

## Related

- Record a refuelling entry
- Assign a refuelling to a vehicle, driver, or cost center
- Correct a refuelling record
- Fuel cost overview and reporting

# Record a refuelling entry

This article shows you how to record a single refuelling event — the litres, the price, the odometer reading, and the receipt — whether the fuel was dispensed from one of your own tanks or bought externally on a fuel card. You need the **tankmanagement:refuel** permission, held by FleetManager and SystemAdmin by default.

## Two ways to record a fill-up

The module distinguishes where the fuel came from:

- **From a company tank** — record the refuelling against a specific tank. The quantity you dispense is subtracted from that tank's current level (it never drops below zero).
- **External refuelling** — record a stand-alone entry with no tank, for a fill-up at a public station, typically paid on a fuel card.

## Fields on a refuelling entry

| Field | Required | Notes |
| --- | --- | --- |
| Quantity (litres) | Yes | Must be greater than zero. |
| Refuelling date | Yes | Cannot be more than one day in the future. |
| Cost per litre | No | Cannot be negative. |
| Total cost | No | Cannot be negative. |
| Odometer reading | No | Cannot be negative; drives per-vehicle consumption. |
| Vehicle | No | Links the fill-up to a vehicle. |
| Driver name | No | Free text, up to 200 characters. |
| Receipt number | No | Carried onto the linked expense document. |
| Fuel card | No | Links the fill-up to a provider card. |
| Cost center | No | Used for cost-analysis and budget matching. |
| Notes | No | Free text, up to 1000 characters. |

### Recording the entry

1. Open the fuel module and choose to record a refuelling — either from a tank, or as an external refuelling.
2. Enter the quantity in litres and the refuelling date.
3. Enter the price. You can supply *cost per litre*, *total cost*, or both — see the tip below.
4. Add the odometer reading, vehicle, driver, receipt number, fuel card, and cost center where you have them.
5. Save. The record is written and an audit entry is logged against your user.

> **Tip:** You only need one of the two price fields. If you enter cost per litre, the module multiplies it by the litres to fill in the total. If you enter the total, it divides by the litres to derive the cost per litre.

## What happens after you save

- If you entered any price, a linked Beleg (receipt/expense document) of type *fuel* is created as a draft in EUR, with the receipt number, odometer as the invoice mileage, the vehicle, and a single fuel line item. This is how the cost reaches expense reporting.
- For a tank refuelling, the tank's current level is reduced by the litres dispensed, which can trigger a low-level alert on the dashboard.

> **Note:** There is no dedicated "station" field on a manual entry. For an external fill-up, record the station in the Notes field; the linked expense document shows the vendor as the tank name or "External Refueling".

## Related

- Assign a refuelling to a vehicle, driver, or cost center
- Correct a refuelling record
- Import fuel-card transactions
- Track fuel consumption per vehicle and tank

# What fuel management (Tankmanagement) does

Fuel management (Tankmanagement) is the MobilityManager module for recording every litre of fuel your fleet uses, what it costs, and where it came from. This article explains what the module tracks and who can work with it.

## What the module covers

Tankmanagement brings your on-site tanks, fuel cards, refuelling events, and fuel budgets together so that consumption and cost stay auditable per vehicle and per cost center (Kostenstelle). Working areas are:

- **Tanks** — company storage tanks with a fuel type, capacity, current level, and a minimum-level threshold that drives low-level alerts.
- **Refuelling records** — a single fill-up, either dispensed from one of your tanks or bought externally at a public station on a fuel card.
- **Fuel cards** — provider cards assigned to a vehicle or driver, with an optional monthly limit and expiry date.
- **Fuel-card transaction import** — a staged upload workflow that turns a provider file (DKV, UTA, or generic CSV) into draft expense documents.
- **Fuel prices** — dated price-per-litre entries per fuel type, so you always have a current reference price.
- **Budgets** — a spend cap for a period, optionally scoped to a cost center and fuel type, tracked against actual spend.
- **Dashboard and reports** — a live overview plus consumption, cost-analysis, and budget-versus-actual reports, with CSV exports.

## How the pieces connect

When you record a refuelling that includes cost data, the module automatically creates a linked Beleg (receipt/expense document) of type *fuel* so the money flows into your expense and financial reporting. If the fuel came from a company tank, that tank's current level is reduced by the quantity dispensed. Refuelling records carry an optional vehicle, driver name, odometer reading, receipt number, fuel card, and cost center, which is what makes per-vehicle and per-cost-center reporting possible.

> **Note:** Tankmanagement is a licensed module. If you do not see the fuel area in the navigation, your company may not have the module entitlement, or your role may lack the required permission.

## Who can use it

Access is controlled by three permissions rather than by role name:

| Permission | Grants |
| --- | --- |
| tankmanagement:view | See tanks, refuelling history, fuel cards, prices, budgets, the dashboard, reports, and exports. |
| tankmanagement:refuel | Record refuelling events and tank level readings. |
| tankmanagement:manage | Create and edit tanks, fuel cards, fuel prices, budgets, and run the fuel-card import workflow. |

By default both the **FleetManager** and **SystemAdmin** roles hold all three permissions. The **Driver** role has none of them, so fuel data stays within fleet administration. You can grant any of the three to a custom role.

## Auditing

Creating, updating, or deactivating a tank, fuel card, or refuelling record writes an entry to the module audit log with the acting user and a trace ID, giving you a tamper-evident history of who changed what.

## Related

- Record a refuelling entry
- Import fuel-card transactions
- Fuel cost overview and reporting
- Track fuel consumption per vehicle and tank

# The audit trail and what it captures

MobilityManager keeps an audit trail of changes to your tenant data so you can see who changed what and when. Entries are written automatically and cannot be altered afterward. This article explains what the audit trail captures and how it is recorded.

## Required permission

Reading the audit trail requires the `settings:view` permission. The trail is read-only; it cannot be edited or deleted through the platform.

## How entries are captured

The audit trail is populated automatically whenever tenant data is saved. Every insert, update, and delete on an audited entity produces an audit entry — you do not have to enable logging per record. Each entry records the acting user's email and ID along with the change details, and entries are stored in the tenant's own database, so they are scoped to that tenant.

## What each entry contains

| Field | Description |
| --- | --- |
| AuditId | A unique identifier that groups related changes saved together in one operation. |
| EntityType | The kind of record that changed, for example `Vehicle`, `Booking`, or `Driver`. |
| EntityId | The primary key of the changed record. |
| Action | One of `Insert`, `Update`, or `Delete`. |
| Timestamp | When the change occurred, in UTC. |
| UserEmail / UserId | The email and ID of the user who made the change. |
| OldValues | The values before the change. Empty for inserts. |
| NewValues | The values after the change. Empty for deletes. |
| ChangedColumns | For updates, the specific columns that changed, each with its original and new value. |
| TableName | The database table the record belongs to. |
| AdditionalInfo | Optional extra context such as a correlation identifier. |

> 
> **Note:** For an update, only the columns that actually changed appear in `ChangedColumns`, so you can see the precise before-and-after of each modified field.

## What is not audited

A few record types are excluded to avoid noise and loops, because they are themselves a form of history:

- Audit entries (the trail does not audit itself).
- Belege (receipts) workflow events.
- Retention execution logs.
- Vehicle history entries (Fahrzeugakte history).

Records that were loaded but not changed are also skipped — only real inserts, updates, and deletes are recorded.

> 
> **Tip:** Use the `AuditId` to reconstruct a single business action that touched several records at once — all of its audit entries share the same identifier.

## Related

- View and filter audit entries
- View the audit history of an entity
- Why audit entries cannot be edited or deleted
- Retention and compliance reporting

# View a user's audit history

To review everything a particular person did in your tenant, MobilityManager provides a per-user audit history. It returns the changes made by one user's email, newest first, with an optional date range. This is useful for access reviews, incident investigations, and responding to data-subject requests. This article explains how to use it.

## Required permission

Viewing a user's audit history requires the `settings:view` permission. Access is read-only and scoped to your current tenant.

## How to retrieve a user's history

1. Identify the user's email address.
2. Call `GET /api/audit/user/{userEmail}`.
3. Optionally add `fromDate` and `toDate` query parameters to restrict the results to a date-and-time window (inclusive).

Entries are returned from most recent to oldest.

> 
> **Note:** For safety, this view returns up to 1,000 of the user's most recent entries. If someone made more changes than that within your date range, narrow the range with `fromDate` and `toDate` to be sure you see everything.

## What each entry shows

Each entry records the action (`Insert`, `Update`, or `Delete`), the affected entity type and ID, the UTC timestamp, and — for updates — the specific columns that changed with their old and new values. Together this gives a chronological record of that user's activity.

## Users who have been pseudonymized

When a user is pseudonymized for GDPR erasure, their email on past audit entries is replaced with the pseudonym, for example `Deleted User #00042-58391`. The audit entries themselves are preserved — only the identity is removed. To find an erased user's history after the fact, search by the pseudonym rather than the original email.

> 
> **Tip:** The history is matched on the exact email string. If a user's email address was changed over time, run the query once for each address they used.

## Related

- View and filter audit entries
- View the audit history of an entity
- Pseudonymize a user (GDPR right to erasure)
- The audit trail and what it captures

# View and filter audit entries

The audit trail can grow large, so MobilityManager lets you filter and page through entries to find the changes you care about. This article explains the available filters, how paging works, and how to open a single entry.

## Required permission

Viewing audit entries requires the `settings:view` permission. All audit access is read-only and scoped to your current tenant.

## Listing entries

Request the audit list from `GET /api/audit`. Results are returned newest first (by timestamp), together with the total number of matching entries so you can build paging controls.

## Available filters

Combine any of these query parameters to narrow the results:

| Filter | Purpose |
| --- | --- |
| `entityType` | Only changes to a given record type, for example `Vehicle` or `Booking`. |
| `entityId` | Only changes to a specific record, identified by its primary key. |
| `action` | Only a given action: `Insert`, `Update`, or `Delete`. |
| `userEmail` | Only changes made by a specific user's email. |
| `userId` | Only changes made by a specific user ID. |
| `fromDate` / `toDate` | Restrict to a date-and-time range (inclusive). |

## Paging through results

Two parameters control the page:

- `skip` — how many entries to skip; defaults to `0`.
- `take` — how many entries to return; defaults to `50` and is capped at `100`.

To move through pages, increase `skip` by your page size on each request. Compare the running total you have retrieved against the reported total count to know when you have reached the end.

> 
> **Note:** Requesting a `take` larger than 100 does not return more than 100 entries — the page size is limited for performance.

## Opening a single entry

When you have an entry's numeric ID, retrieve just that entry with `GET /api/audit/{auditEntryId}`. The response includes the full detail: old and new values, and the changed columns for updates. If the ID does not exist, the request returns "Audit entry not found".

> 
> **Tip:** To investigate one record's full timeline, filter by both `entityType` and `entityId`, or use the dedicated entity history view.

## Related

- The audit trail and what it captures
- View the audit history of an entity
- View a user's audit history
- Why audit entries cannot be edited or deleted

# View the audit history of an entity

When you need the full change history of one specific record — a single vehicle, booking, or driver — MobilityManager provides a dedicated entity history view. It returns every audit entry for that record in one call, newest first. This article explains how to use it.

## Required permission

Viewing entity audit history requires the `settings:view` permission. Access is read-only and scoped to your current tenant.

## How to retrieve an entity's history

1. Identify the record's **entity type** — the kind of record, such as `Vehicle`, `Booking`, or `Driver`.
2. Identify the record's **entity ID** — its primary key value.
3. Call `GET /api/audit/entity/{entityType}/{entityId}`.

The response lists all matching audit entries ordered from most recent to oldest, along with a count of how many were returned.

## What you see for each change

Every entry in the history includes the action (`Insert`, `Update`, or `Delete`), the timestamp in UTC, and the user who made the change. For updates, the entry shows exactly which columns changed together with their original and new values, so you can trace how the record evolved field by field.

> 
> **Tip:** The first (oldest) entry for a record is usually its `Insert`, and the newest entry sits at the top. Reading from the bottom up reconstructs the record's life from creation to its current state.

## Entity type and ID must match exactly

The entity type is matched by name, so use the exact type as recorded in the audit trail (for example `Vehicle`, not `vehicles`). The entity ID is the record's primary key. If either value does not match, the history comes back empty rather than as an error.

> 
> **Note:** Unlike the general audit list, entity history is not paged — it returns the complete set of entries for the record. For very active records, this can be a long list.

## When to use this instead of the filtered list

The filtered audit list is best for browsing across many records or users. The entity history view is best when you already know the exact record and want its complete, self-contained timeline in a single request.

## Related

- View and filter audit entries
- The audit trail and what it captures
- View a user's audit history
- Why audit entries cannot be edited or deleted

# GDPR compliance in MobilityManager

MobilityManager gives administrators the tools they need to meet core GDPR (DSGVO) obligations for the personal data your fleet holds about users and drivers. This overview explains the two building blocks the platform provides — the right to erasure and the audit trail — and where the relevant personal data lives.

## The two GDPR building blocks

Compliance in MobilityManager rests on two capabilities that work together:

- **Right to erasure (GDPR Art. 17)** — a user's personal data is pseudonymized, meaning identifying fields are replaced with a non-reversible placeholder such as `Deleted User #00042-58391`. The record structure is preserved so that history and reporting stay intact.
- **Audit trail** — every create, update, and delete on tenant data is recorded automatically in an append-only log that captures who changed what, when, and the before/after values. Audit entries are immutable.

## Where personal data is stored

Because MobilityManager is multi-tenant, personal data is spread across two kinds of database, and erasure must reach both:

| Location | Personal data held |
| --- | --- |
| System database | The login account: username, email, first and last name, password hash, password-reset tokens, and OIDC identifiers. |
| Per-tenant databases | The tenant-specific *Driver* record, the *DriverLicenseProfile* (the regulated Führerschein / driver-license number), and *AuditEntry* rows that reference the user's email. |

Pseudonymization scrubs personal data in every tenant the user belonged to, not just the current one.

## Who can perform GDPR actions

GDPR erasure actions require the `users:manage` permission, and the requesting account must also hold the **SystemAdmin** role. Reading the audit trail requires the `settings:view` permission. Roles in MobilityManager are **SystemAdmin**, **FleetManager**, and **Driver**, plus any custom roles your organization defines.

> 
> **Note:** Pseudonymization is irreversible by design. Once a user is pseudonymized, the original identifying values cannot be recovered.

## What is preserved

Erasure removes personal data but keeps the surrounding structure so your organization can still demonstrate compliance. For example, audit entries keep their timestamp, action, entity type, and entity ID — only the user's email is replaced with the pseudonym. This lets you prove that a change happened and when, without exposing who the person was.

> 
> **Tip:** Before submitting an erasure request, use the eligibility check to confirm the account can be pseudonymized. This avoids failed requests for system administrators or already-erased accounts.

## Related

- Pseudonymize a user (GDPR right to erasure)
- Check whether a user can be pseudonymized
- The audit trail and what it captures
- Why audit entries cannot be edited or deleted

# Retention and compliance reporting

Meeting data-protection obligations means keeping records long enough to prove compliance, but not indefinitely. MobilityManager combines an immutable audit trail, GDPR pseudonymization, and governed retention runs so you can both report on activity and dispose of data on schedule. This article explains how those pieces fit together for compliance reporting.

## Required permissions

- `settings:view` — to read the audit trail and produce activity reports.
- `users:manage` plus the **SystemAdmin** role — to run GDPR erasure.
- `belege:compliance-trail` — to view the Belege (receipts) compliance dashboard, including retention run status and the access trail.

## Reporting from the audit trail

The audit trail is your primary source of compliance evidence. You can assemble reports by:

- Filtering audit entries by entity type, action, user, or date range.
- Pulling the full history of a single record with the entity history view.
- Pulling everything one user did with the per-user history view.

Because entries are immutable and timestamped in UTC, the reports you build from them are defensible.

## Governed retention runs

In the Belege (receipts) compliance area, MobilityManager records the outcome of automated retention runs, which delete data that has passed its retention window. Each run is logged with:

| Field | Meaning |
| --- | --- |
| Run time | When the retention run executed. |
| Outcome | Whether the run succeeded or failed (with an error message on failure). |
| Bookings deleted | How many booking records were purged. |
| Audit entries deleted | How many audit entries were purged. |

You can review the most recent runs from the compliance status view. This gives you an auditable record that retention actually happened.

> 
> **Note:** Retention runs are a governed, logged process. This is different from manual deletion of audit entries, which is always blocked. The audit trail stays immutable; only the scheduled retention process disposes of aged data, and it records what it removed.

## Reporting on erasure requests

When you fulfill a GDPR right-to-erasure request, the pseudonymization operation returns statistics — the pseudonym assigned and the number of user, driver, and audit records updated. Capture these figures as evidence that the request was completed across all databases.

> 
> **Tip:** For a complete compliance picture, pair the audit trail (what changed and by whom) with the retention run log (what was disposed of and when) and your record of fulfilled erasure requests.

## Related

- Why audit entries cannot be edited or deleted
- The audit trail and what it captures
- GDPR compliance in MobilityManager
- Pseudonymize a user (GDPR right to erasure)

# Why audit entries cannot be edited or deleted

The audit trail in MobilityManager is append-only: entries can be read, but never edited or deleted through the platform. This immutability is what makes the trail trustworthy as compliance evidence. This article explains how the platform enforces it and the one narrow exception that exists for GDPR.

## Required permission

Reading audit entries requires the `settings:view` permission. There is no permission that grants the ability to change or remove them — those operations are not available to anyone.

## Why the trail is immutable

An audit trail only has value if it cannot be tampered with. If a user could edit or delete entries, they could hide their own actions, and the trail could no longer prove what happened. By making the trail append-only, MobilityManager guarantees that once a change is recorded, the record of that change stays intact.

## How it is enforced

Attempts to modify the audit trail are actively rejected:

| Attempt | Result |
| --- | --- |
| Edit an audit entry (`PUT /api/audit/{id}`) | Blocked with "Audit entries are immutable and cannot be modified" and the attempt is logged. |
| Delete an audit entry (`DELETE /api/audit/{id}`) | Blocked with "Audit entries are immutable and cannot be deleted" and the attempt is logged. |

The underlying audit table is itself append-only, so the trail is protected at both the interface and the data layer.

> 
> **Note:** The audit trail does not audit itself. Because entries can never change, there is no need to record changes to them — and this also prevents an endless loop of audit-on-audit entries.

## The one exception: GDPR pseudonymization

GDPR's right to erasure requires removing a person's identity even from historical records. To reconcile this with immutability, pseudonymization makes the single minimal change it is allowed to: it replaces the `UserEmail` on that person's past audit entries with a pseudonym such as `Deleted User #00042-58391`. Every other field — timestamp, action, entity type, entity ID, and the recorded values — is preserved. The structure of the trail stays intact; only the personal identifier is removed.

> 
> **Tip:** Scheduled data-retention runs may purge entries older than your retention window. That is a governed, logged process — not ad-hoc deletion — and it is separate from the blocked manual delete described above.

## Related

- The audit trail and what it captures
- Retention and compliance reporting
- Pseudonymize a user (GDPR right to erasure)
- View and filter audit entries

# Check whether a user can be pseudonymized

Before you submit a GDPR erasure request, MobilityManager lets you check whether the target account is eligible for pseudonymization. This validation runs the same rules as the erasure itself but changes nothing, so you can confirm eligibility from your interface before committing. This article explains how to run the check and what each result means.

## Required role and permission

The eligibility check requires the `users:manage` permission. Like the erasure operation, it also confirms that your account holds the **SystemAdmin** role.

## How to run the check

1. Identify the user ID you want to test.
2. Call `GET /api/gdpr/can-pseudonymize/{userId}`.
3. Read the response: `canPseudonymize` is `true` when the account is eligible, or `false` with an `errorMessage` explaining why not.

> 
> **Tip:** Always run this check first. It prevents failed erasure requests and gives you a clear reason to communicate back to the person who raised the request.

## Reasons a user cannot be pseudonymized

The check returns `false` in the following cases:

| Result message | Meaning |
| --- | --- |
| Cannot pseudonymize your own user account | You cannot erase the account you are signed in with. Ask another SystemAdmin to process it. |
| Forbidden: requesting user must be a SystemAdmin to pseudonymize users | Your account does not hold the SystemAdmin role, so the operation is blocked even if you have `users:manage`. |
| User not found | No user exists with that ID. |
| Cannot pseudonymize a SystemAdmin user. Remove SystemAdmin role first. | The target account is itself a SystemAdmin. Remove that role before erasing. |
| User has already been pseudonymized | The account's email or username already begins with `Deleted User #`, so there is nothing left to scrub. |

## What a positive result means

When `canPseudonymize` is `true`, the account can proceed to erasure. The check validates eligibility only — no personal data is changed until you submit the actual pseudonymization request.

> 
> **Note:** If a SystemAdmin needs to be erased, first remove the SystemAdmin role from their account, then re-run this check. Once the role is gone, the account becomes eligible.

## Related

- Pseudonymize a user (GDPR right to erasure)
- Who can run GDPR actions
- GDPR compliance in MobilityManager

# Pseudonymize a user (GDPR right to erasure)

When a user exercises their GDPR Art. 17 right to erasure ("right to be forgotten"), MobilityManager pseudonymizes their personal data rather than deleting records outright. This replaces identifying fields with a non-reversible placeholder while preserving the record structure needed for compliance. This article explains exactly what the operation scrubs and how to run it.

## Required role and permission

You need the `users:manage` permission, and your account must hold the **SystemAdmin** role. The service enforces the SystemAdmin requirement in addition to the permission check, so a FleetManager who happens to have `users:manage` still cannot complete the operation.

## How to pseudonymize a user

1. Confirm the target user's ID.
2. Run the eligibility check first (`GET /api/gdpr/can-pseudonymize/{userId}`) to make sure the account is eligible.
3. Submit the erasure request to `POST /api/gdpr/pseudonymize` with the body `{ "userId": <id> }`.
4. Record the returned pseudonym ID and the statistics (users, drivers, and audit entries updated) as evidence that the request was fulfilled.

The generated pseudonym looks like `Deleted User #00042-58391`, combining the user ID with a random suffix so it is unique.

## Exactly what data is scrubbed

The operation reaches both the system database (the login account) and every tenant database the user belonged to.

| Database | Record | What changes |
| --- | --- | --- |
| System | User | Username and email set to the pseudonym; first and last name cleared; password hash emptied; password-reset token and expiry cleared; OIDC subject and provider cleared; account deactivated. All tenant role assignments are removed, so the user can no longer sign in. |
| Tenant | Driver | First name set to "Deleted", last name to "User", email set to the pseudonym; phone, license number, and license expiry date cleared; the driver is deactivated. |
| Tenant | DriverLicenseProfile | The regulated Führerschein (driver-license) number and issuing country code are emptied. This is the authoritative license number and is scrubbed separately so no strong identifier is left behind. |
| Tenant | AuditEntry | The user's email on their past actions is replaced with the pseudonym. All other audit fields are preserved. |

> 
> **Warning:** Pseudonymization is irreversible. The original username, email, names, and license numbers cannot be recovered afterward. Confirm you have the correct user ID before submitting.

## Why the tenant list is captured first

The operation reads the user's tenant memberships *before* removing their role assignments. If it read them afterward the list would be empty, and driver and license data would be silently left in place. Capturing the tenants first guarantees that per-tenant PII is erased everywhere the user was a member.

> 
> **Note:** Audit history is intentionally kept. Timestamps, actions, entity types, and entity IDs remain so your organization can still demonstrate what happened, only without the person's identity attached.

## Related

- Check whether a user can be pseudonymized
- Who can run GDPR actions
- GDPR compliance in MobilityManager
- View a user's audit history

# Who can run GDPR actions

GDPR erasure actions in MobilityManager are deliberately restricted, because pseudonymizing a user is irreversible and touches personal data across multiple databases. This article explains exactly who is allowed to run these actions and the two layers of protection that enforce it.

## The permission: users:manage

Both GDPR endpoints — the eligibility check and the erasure itself — require the `users:manage` permission. Without it, the request is rejected before any processing happens. This is the same permission that governs managing user accounts elsewhere in the platform.

## The role requirement: SystemAdmin

Holding `users:manage` is necessary but not sufficient. The pseudonymization service performs a second, independent check: the requesting account must hold the **SystemAdmin** role. This is defense in depth — even if a lower-privileged account somehow reached the service with the permission attached, the operation still stops with a "Forbidden" result.

| Role | Can run GDPR erasure? |
| --- | --- |
| SystemAdmin | Yes, provided the account also has `users:manage`. |
| FleetManager | No. Even with `users:manage`, the SystemAdmin check blocks the operation. |
| Driver | No. |
| Custom roles | No, unless the role is combined with SystemAdmin. |

## Additional guardrails

Beyond who you are, the platform enforces rules about who you may erase:

- You cannot pseudonymize your own account. Ask another SystemAdmin to process it.
- You cannot pseudonymize another SystemAdmin. Remove the SystemAdmin role from that account first, then proceed.
- You cannot pseudonymize an account that has already been pseudonymized.

> 
> **Note:** Reading the audit trail is governed by a different permission, `settings:view`. Being able to view audit entries does not grant the ability to run GDPR erasure, and vice versa.

> 
> **Tip:** To let a colleague process erasure requests, assign them the SystemAdmin role and make sure their permission set includes `users:manage`. Both are required together.

## Related

- Pseudonymize a user (GDPR right to erasure)
- Check whether a user can be pseudonymized
- GDPR compliance in MobilityManager
- View and filter audit entries

# Glossary of key German terms used across MobilityManager

MobilityManager keeps a number of German domain terms even in the English interface, because they are the established words for fleet processes in the DACH region. This glossary explains the key ones so you recognize them across the product.

## Modules and areas

| Term | English | What it means |
| --- | --- | --- |
| Fuhrpark | Vehicle fleet | Your collection of vehicles. The Fuhrpark dashboard gives a fleet-wide overview. |
| Fahrzeugakte | Vehicle file | The complete record for a single vehicle, organized into tabs (technical data, contracts, tyres, appointments, and more). |
| Belege | Receipts / expense documents | The Expenses module. A Beleg is one receipt or expense document that moves through a review workflow. |
| Strafmandate | Penalty notices | Traffic fines and penalty notices. A single one is a Strafmandat. |
| Führerscheinkontrolle | Driving-licence check | The process of verifying that drivers hold a valid driving licence, with uploads and periodic re-checks. |
| Versicherung | Insurance | The insurance module: policies, claims, certificates, and risk. |
| Schadenmanagement | Damage management | Recording and tracking vehicle damage cases through their lifecycle. |
| Wartungsplanung | Maintenance planning | Planning and recording vehicle maintenance, with templates, plans, and reminders. |
| Tankmanagement | Fuel / tank management | Refueling, fuel cards, fuel prices, and fuel budgets. |
| Lagerverwaltung | Warehouse management | Stock items, stock movements, and inventory counts. |
| E-Mail-Vorlagen | Email templates | Reusable templates for the emails the platform sends. |

## Vehicle-file tabs and details

| Term | English | What it means |
| --- | --- | --- |
| Kennzeichen | Licence plate / registration | A vehicle's registration number. |
| Technik | Technical data | Technical specifications of the vehicle. |
| Verträge | Contracts | Contracts associated with a vehicle, such as leasing agreements. |
| Termine | Appointments / due dates | Scheduled dates and deadlines for a vehicle. |
| Reifen | Tyres | Tyre records for a vehicle. |
| Vignetten | Toll stickers | Road-toll stickers (Vignette) held for a vehicle. |

## Regulatory frameworks

During setup, each tenant selects the periodic vehicle-inspection regime it follows. These abbreviations appear in settings and reports:

| Code | Region | Meaning |
| --- | --- | --- |
| HU | Germany | Hauptuntersuchung — the main periodic roadworthiness inspection. |
| §57a | Austria | The §57a periodic inspection, commonly known as the "Pickerl". |
| MFK | Switzerland | Motorfahrzeugkontrolle — the official vehicle inspection. |

> **Tip:** The help center search matches these German terms directly, so you can search for "Belege" or "Strafmandate" to jump to the relevant articles even when the rest of your interface is in English.

> **Note:** Switching your interface language between German and English changes many labels, but these established domain terms are used consistently in both languages so you always recognize the module.

## Related

- What MobilityManager is and who it is for
- A tour of the main navigation and sidebar
- Changing the interface language (German or English)
- Understanding companies and tenants

# The three system roles at a glance

This article introduces the three built-in roles in MobilityManager, what each can do, and how custom roles extend the model. Understanding roles helps you know why you can see some features and not others.

## How access works

Every action in the platform is protected by a **permission**. A role is simply a named bundle of permissions, and roles are assigned to you per tenant. When you open a page or select a button, the system checks whether your role includes the required permission. If it does not, the feature is hidden or refused. This is why your menu can differ from a colleague's.

## The three built-in roles

| Role | Who it is for | What it can do |
| --- | --- | --- |
| **SystemAdmin** | Platform owner / IT | Full access. Automatically holds every permission in the system, including managing tenants, users, roles, licence, and settings, and the most sensitive actions such as financial corrections and budget management. |
| **FleetManager** | Fleet or mobility coordinator | Runs fleet operations end to end: create and manage vehicles, approve and reject bookings, work with damage cases, CRM, Strafmandate (penalty notices), Belege (receipts/expense documents), driving-licence compliance, and the operations modules. |
| **Driver** | Employees who use vehicles | Self-service only: create bookings, view their own bookings, view the vehicles they can book, and view and upload their own driving licence for the Führerscheinkontrolle (licence check). |

### What SystemAdmin can do that FleetManager cannot

FleetManager is deliberately broad but not unlimited. A few high-sensitivity capabilities are reserved for SystemAdmin, including financial corrections on Belege, fleet-cost budget management, and administration of roles and tenants. This keeps financially and structurally sensitive actions with the platform owner.

### What the Driver role deliberately excludes

The Driver role does not include access to the Expenses (Belege) area. Belege is a finance and administration function, so it stays with managers and administrators rather than every driver.

> **Note:** SystemAdmin always receives newly added permissions automatically. When a new module is introduced, administrators keep full access to it without any manual change, while other roles are granted access deliberately.

## Custom roles

The three roles above are a starting point. A SystemAdmin can create **custom roles** from **Administration → Roles & Permissions** and give each one a tailored set of permissions — useful when a job needs more than a Driver but less than a full FleetManager. Note that:

- The three built-in roles are system roles: their permission sets are fixed and cannot be edited or deleted.
- Custom roles can be created, edited, given specific permissions, and removed.
- Only a SystemAdmin can assign or remove the SystemAdmin role.

> **Tip:** Assigning the Driver role to a user automatically creates a driver profile for them in that tenant, so they are ready to book straight away.

## Related

- Understanding companies and tenants
- What MobilityManager is and who it is for
- A tour of the main navigation and sidebar
- Where to get help in MobilityManager

# Understanding companies and tenants

This article explains what a tenant is in MobilityManager, how tenants relate to users and roles, and how to work across more than one company.

## What a tenant is

MobilityManager is multi-tenant. A **tenant** is a company or organizational unit that keeps its own isolated data — its vehicles, drivers, bookings, expenses, and settings are separate from every other tenant. One installation of the platform can host several tenants side by side without their data mixing. In everyday language you can read "tenant" as "company."

Each tenant has a few defining properties:

| Property | Meaning |
| --- | --- |
| Name | The company's display name. |
| Code (identifier) | A short code that identifies the tenant, including at sign-in. |
| Status | Whether the tenant is Active. |
| Language | The tenant's default language (DE or EN). |
| Regulatory framework | The inspection regime the fleet follows: HU, §57a, or MFK. |

## How users and roles fit in

Users exist at the platform level, but roles are assigned *per tenant*. That means the same person can belong to several tenants and hold a different role in each — for example FleetManager in one company and Driver in another. Your permissions are always evaluated against the tenant you are currently working in.

> **Note:** Because roles are per-tenant, being an administrator in one company does not grant you access to another company's data. Each tenant's access is granted separately.

## Choosing a tenant at sign-in

When you sign in, you select which company to work in. If your installation has only one tenant, it is chosen automatically. If there are several, pick one from the tenant selector, shown as "Name (code)." An administrator can also share a direct link that pre-selects a specific tenant by its code.

## Switching tenants after sign-in

If you have access to more than one tenant, use the **tenant switcher** in the top bar to move between them without signing out. The dashboard and every module reload to show the newly selected company, and the hero banner on the dashboard always names the tenant you are currently in.

## Creating and managing tenants

Your first tenant is created during onboarding. Additional tenants are managed from **Administration → Tenants** and require the appropriate tenant-management permission (typically held by a SystemAdmin). The number of tenants you may create is capped by your licence — for example, a demo licence allows a single tenant.

> **Tip:** If you need a separate space for a different company, subsidiary, or department rather than a new folder inside your existing company, a new tenant is usually the right choice — provided your licence permits it.

## Related

- The three system roles at a glance
- Your first login after setup
- What MobilityManager is and who it is for
- The onboarding wizard: admin account, licence, tenant, and settings

# What MobilityManager is and who it is for

This article explains what MobilityManager is, the problems it solves, and who typically uses it, so you can decide where you fit and what to read next.

## What MobilityManager is

MobilityManager is a web-based fleet and mobility management platform for organizations that operate a Fuhrpark (vehicle fleet). It brings the day-to-day work of running vehicles, drivers, bookings, costs, and compliance into a single application that you open in your browser. There is nothing to install on your computer.

The platform is multi-tenant, which means one installation can serve several separate companies or organizational units. Each of these is called a tenant, and every tenant keeps its own isolated data. When you sign in, you always work inside one tenant at a time.

## What you can do with it

The features you see depend on your role, your permissions, and which modules your organization has licensed. Commonly available areas include:

- **Fleet and vehicles** — maintain the Fahrzeugakte (vehicle file) for each vehicle, group vehicles into pools, and manage brands and models.
- **Bookings** — reserve vehicles, approve or reject requests, and view a shared booking calendar.
- **Expenses** — capture and review Belege (receipts/expense documents) through a structured review workflow.
- **Compliance** — run the Führerscheinkontrolle (driver's licence check), track Strafmandate (penalty notices), and manage damage cases.
- **Operations add-ons** — optional modules such as Versicherung (insurance), Wartungsplanung (maintenance planning), Tankmanagement (fuel management), and Lagerverwaltung (warehouse management).

## Who it is for

MobilityManager recognizes three built-in roles, and your role determines what you can see and do:

| Role | Typical user | Focus |
| --- | --- | --- |
| SystemAdmin | IT or platform owner | Full configuration: tenants, users, roles, licence, and every module. |
| FleetManager | Fleet or mobility coordinator | Running fleet operations end to end: vehicles, bookings, compliance, expenses, and reports. |
| Driver | Employee who uses vehicles | Self-service: booking a vehicle, seeing their own bookings, and keeping their own driving licence up to date. |

Your organization can also create custom roles with a tailored set of permissions, so these three are a starting point rather than a hard limit.

> **Note:** Access throughout the platform is controlled by permissions, not just role names. If a menu item or button is missing for you, it usually means your role does not include the required permission or your organization has not licensed that module.

## How it is delivered

The interface is a modern single-page web application. You interact with it entirely through your browser, it is available in German and English, and it adapts to the regulatory framework your organization selects during setup (for example HU, §57a, or MFK vehicle-inspection regimes).

## Related

- The three system roles at a glance
- Understanding companies and tenants
- A tour of the main navigation and sidebar
- Glossary of key German terms used across MobilityManager

# Changing the interface language (German or English)

This article explains how to switch the MobilityManager interface between German and English, and how your personal language choice relates to your company's default language.

## Supported languages

The interface is available in two languages:

- **English (EN)**
- **German / Deutsch (DE)**

Your choice affects labels, menus, and messages throughout the application. It does not translate the data you enter, such as vehicle names or notes.

## Changing the language while signed in

1. Open the **profile menu** from your avatar (your initials) in the top-right corner.
2. Select your preferred language, German or English.
3. The interface updates immediately — there is no need to reload the page or sign in again.

## Changing the language before you sign in

You do not have to be signed in to switch languages. The sign-in page has its own language switch, so you can read the login screen in your preferred language first and it will carry through after you sign in.

## How your choice is remembered

Your language preference is stored in your browser, so the next time you open the application on the same browser it starts in the language you last chose. If you have never picked one, the app falls back to your browser's language when it is German or English, and otherwise defaults to English.

> **Note:** Because the preference is saved in the browser's local storage, it is per-browser and per-device. If you sign in from a different computer or browser, set your language again there. Clearing your browser data also resets the choice.

## Personal language vs. tenant default language

There are two related but separate settings, and it helps to keep them apart:

| Setting | Scope | Where it is set |
| --- | --- | --- |
| Interface language | Your personal preference, per browser | The profile menu (or the sign-in page) |
| Tenant default language | The company's default, chosen at setup | The onboarding settings step, as DE or EN |

The tenant default language is picked when the company is first configured. Your personal interface-language choice applies to your own session and overrides the default for you, without changing what other users see.

> **Tip:** If colleagues report seeing the app in a different language than you do, that is expected — each person's interface language is their own browser preference.

## Related

- A tour of the main navigation and sidebar
- Your first login after setup
- Glossary of key German terms used across MobilityManager

# A tour of the main navigation and sidebar

This article gives you a tour of the main navigation so you can find your way around MobilityManager: the sidebar, the top bar, and the tools that help you move quickly.

## The left sidebar

The sidebar is your primary menu. It groups modules into collapsible sections such as Home, Fleet, CRM, Insurance (Versicherung), Bookings, Expenses (Belege), Strafmandate, License Compliance, Warehouse (Lagerverwaltung), Maintenance (Wartungsplanung), Tank Management, Reports & Analytics, and Administration.

Sections work like an accordion: select a section header to expand or collapse it. The sidebar only shows what you are allowed to use — items are filtered by your permissions, your role, and the modules your organization has licensed, so two users can see different menus.

### Finding items quickly

- **Search** — type in the search box at the top of the sidebar to filter items by name across all sections.
- **Favorites** — star an item to pin it to a quick-access area at the top. There is a limit on how many favorites you can keep, so reserve them for the pages you use most.
- **Recently visited** — the sidebar remembers pages you opened recently for fast return trips.
- **Collapse** — collapse the sidebar to a slim icon-only rail to give the content area more room, and expand it again when you need labels.

### Badges

Some items show a live count badge so you can spot work waiting for you without opening each page. Badges appear on:

- **Pending Requests** — booking requests awaiting manager assignment.
- **Pending Approvals** — bookings awaiting approval.
- **Review Queue** — Belege (receipts/expense documents) currently in review.

These counts refresh automatically while you work.

## The top bar

Across the top of the workspace you will find:

- **Tenant switcher** — if you belong to more than one company/tenant, switch the active tenant here.
- **Notification bell** — opens your notifications.
- **Theme toggle** — switch between light and dark appearance.
- **Profile menu** — your avatar (shown as your initials) opens a menu where you can change the interface language and sign out.

> **Tip:** On narrow screens (below about 1024 pixels wide) the sidebar collapses behind a menu button. Use that button to open and close navigation on tablets and phones.

## How access shapes what you see

Because navigation is permission-driven, a missing menu item is normal rather than an error. If you need a module that is not listed, a SystemAdmin can either adjust your role and permissions or confirm whether the module is licensed for your organization.

## Related

- Understanding your dashboard
- The three system roles at a glance
- Understanding companies and tenants
- Changing the interface language (German or English)

# Understanding your dashboard

This article explains the dashboard — your home page in MobilityManager — and how it adapts to your role so you see the information that matters to you first.

## Opening the dashboard

The dashboard is where you land after signing in, and you can return to it any time from the Home item in the sidebar. It opens with a hero banner that greets you by name and shows your current company/tenant and today's date, confirming which tenant you are working in.

## What managers see

If you hold the FleetManager or SystemAdmin role, the dashboard surfaces fleet-wide insight:

- **Alerts** — a prioritized list of items needing attention, such as expired or expiring driving licences, active Strafmandate (penalty notices), inspections due, overdue Führerscheinkontrolle (licence check) verifications, open damage cases, and unassigned Belege (receipts/expense documents). Each alert links straight to the relevant page.
- **KPI tiles** — headline numbers including active vs. total vehicles, fleet utilization, driving-licence compliance, open damage cases, open mandates, and unassigned receipts.

Each alert and KPI is a shortcut: select it to jump to the module where you can act on it.

## What drivers see

If you have the Driver role, the dashboard is tuned to self-service:

- **My upcoming bookings** — your next reservations with vehicle, plate, and time, plus a link to see them all.
- **My licence** — a compliance score for your own driving licence, with clear warnings if it is expiring or has expired.

## Quick navigation cards

Below the summaries, a set of quick-navigation cards links to the main areas you can access — for example Fleet, CRM, Expenses, Damage management, Strafmandate, Licence compliance, and Bookings. Like the sidebar, these cards are filtered by your permissions and licensed modules, so you only see cards you can actually open.

> **Note:** The dashboard reflects your permissions. If it looks sparse right after signing in, give it a moment to finish loading; if areas you expected are still absent, your role may not include them.

## Reading the numbers

Counts and scores on the dashboard are drawn from live data in your active tenant. Switching tenants from the top-bar tenant switcher reloads the dashboard for that company, so the figures always describe the tenant named in the hero banner.

## Related

- A tour of the main navigation and sidebar
- The three system roles at a glance
- Understanding companies and tenants
- Your first login after setup

[NextPage](https://support.cloudkasten.net/llms-full.txt/1)
