Ergänzt Auftragserfassung und -bearbeitung, Unternehmerbelege, Stornos, Lagerkorrekturen, Stammdaten, Abrechnung, CRM, Formulare, Berichte und Preisverwaltung auf Basis der aktuellen PHP-Anwendung. Enthält Integrations- und Oberflächentests sowie dokumentierte Abgleiche und Prüfstände.
148 lines
21 KiB
Markdown
148 lines
21 KiB
Markdown
# Spring-Boot-Adapter für die vorhandene VoTiAn-Datenbank
|
||
|
||
Java 21, Spring Boot 3.5.16 und MariaDB JDBC. Dieser Adapter verwendet ausschließlich bestehende Tabellen. Er enthält kein JPA-Modell, keine Schema-Migration, keine eingebettete Datenbank und keine Seed-Daten. Lesen bleibt serverseitig schreibgeschützt; die native Auftragserfassung und freigegebene Verwaltungsprozesse verwenden explizite Schreibgrenzen über denselben Service.
|
||
|
||
## Zentrale Datenbankserviceschicht
|
||
|
||
`database.DatabaseService` ist die einzige Komponente mit Zugriff auf `JdbcTemplate` und den Transaktionsmanager. Authentifizierung, Leseadapter, Auftragserfassung, Protokollierung und Diagnose verwenden ausschließlich diesen Service; auch Integrationstests lesen den Bestand über ihn. Der Service aktiviert `setEnforceReadOnly(true)`, sodass jede Lesetransaktion mit `SET TRANSACTION READ ONLY` serverseitig abgesichert wird. Der JDBC-Hinweis allein reicht hierfür beim verwendeten MariaDB-Treiber nicht aus.
|
||
|
||
- `queryForList(sql, parameters...)` und `queryForObject(sql, type, parameters...)` führen einzelne parametrisierte SELECT-Abfragen in Lesetransaktionen aus.
|
||
- `readTransaction(work)` fasst abhängige Abfragen zusammen; Auftragsliste einschließlich Anzahl und Stationen sowie Auftragsdetails verwenden diese Grenze.
|
||
- `checkConnection()` liefert einen bereinigten Verbindungsstatus und unterscheidet Netzwerkfehler, abgewiesene Anmeldung und fehlende Datenbankrechte.
|
||
- `writeTransaction(work)` erlaubt ausschließlich darin `insert`, `update` und `lockRows`. Auftrag und zugehörige Stationen/Preise/Marker werden gemeinsam committed oder zurückgerollt. DDL und Mehrfachstatements bleiben gesperrt.
|
||
- Das nicht transaktionsfähige Bestandsprotokoll erhält Ereignisse erst nach dem Commit über eine wiederholbare Outbox in `genericdatacontainer`; siehe [Auftragserfassung](../docs/AUFTRAGSERFASSUNG.md).
|
||
|
||
SQL bleibt anwendungseigener Code. Benutzereingaben dürfen nur als gebundene Werte übergeben werden. Der Service weist Nicht-SELECTs, mehrere Statements und bekannte SELECT-Schreib-/Sperrformen über die Lese-API zurück; diese Prüfung ist kein SQL-Parser für beliebige fremde Abfragen. Es werden weder JDBC-Verbindungen noch JDBC-Callbacks nach außen gegeben.
|
||
|
||
`python3 scripts/check-database.py` im Projektstamm startet das gebaute JAR mit `--database-check`. Dieser Modus nutzt denselben Service und dieselbe Poolkonfiguration ohne Webserver, gibt JSON aus und schließt den Spring-Kontext. Python stellt keine eigene Datenbankverbindung mehr her.
|
||
|
||
## Verwaltung und Statistik
|
||
|
||
`/api/administration` gibt die für das Konto freigegebenen Verwaltungsbereiche zurück. Feiertage und Anfahrtszeiten benötigen Recht 2, Gruppen die Rechte 8 und 25; gemeinsame Gruppen/mandantenweite Mitgliedsarten zusätzlich Recht 10. Schreibanfragen verwenden CSRF und Bearbeitungsrevisionen. Die Einzelaktualisierung einer zentralen BWV-PLZ ist wegen der MyISAM-Bestandstabelle nicht mit anderen Schreiboperationen gebündelt. Gruppen dürfen erst ohne Mitglieder gelöscht werden.
|
||
|
||
`GET /api/statistics/options` und `POST /api/statistics/query` benötigen Recht 8. Die Statistik ist rein lesend; POST übermittelt die umfangreichen Filter und benötigt ebenfalls CSRF. Mehrere Niederlassungen sind auf Recht 10 und den bestehenden Mandanten begrenzt. Zeitraum und Vergleiche werden jeweils als vollständige Aggregation abgefragt; die Grenze der Auftragsübersicht spielt dabei keine Rolle. Bei mehr als 5000 Ergebnisgruppen wird die Abfrage abgewiesen. Details und verbleibende Unterschiede: [Menüabgleich](../docs/MENUE-ABGLEICH.md).
|
||
|
||
## Start und Konfiguration
|
||
|
||
Die lokale Datenbankkonfiguration steht in der `.env` im Projektstamm: `DB_URL`, `DB_USER` und `DB_PASSWORD`. `./run.sh`, `./dev.sh` und `python3 scripts/check-database.py` laden diese Datei automatisch. Bereits gesetzte Prozessumgebungsvariablen haben Vorrang vor den Werten aus `.env`.
|
||
|
||
Direkte Maven-Aufrufe laden `.env` nicht automatisch. Für den eigenständigen Backend-Start mit der lokalen Konfiguration im Verzeichnis `backend/` ausführen:
|
||
|
||
```sh
|
||
python3 ../scripts/env_config.py ./mvnw spring-boot:run
|
||
```
|
||
|
||
Verfügbare Konfiguration:
|
||
|
||
| Variable | Bedeutung |
|
||
| --- | --- |
|
||
| `DB_URL` | JDBC-Adresse der bestehenden DB, z. B. `jdbc:mariadb://DB-HOST:3306/DB-NAME?connectTimeout=2000&socketTimeout=5000` |
|
||
| `DB_USER`, `DB_PASSWORD` | Vorhandenes Datenbankkonto; keine Zugangsdaten im Quelltext |
|
||
| `PZM_URL`, `PZM_FALLBACK_URL` | Vollständiger vorhandener `/api/pzm`-Endpunkt und optionaler Ausweichendpunkt für Routen-/Zonenberechnung; alternativ globaler Bestandsparameter `URL_PZM` |
|
||
| `CO2_URL`, `CO2_USERNAME`, `CO2_PASSWORD` | Vorhandener HTTPS-CO₂e-Dienst mit Basic-Authentifizierung; nur serverseitig aus .env/Umgebung |
|
||
| `GOOGLE_MAPS_API_KEY` | Schlüssel für Geokodierung/PZM; alternativ gleichnamiger globaler Bestandsparameter. Nicht versionieren |
|
||
| `HISTORY_DB_SCHEMA` | Bestehendes Protokollschema auf demselben Server, Standard `phoenix_log`; benötigt SELECT-Rechte für `log`/`logoperation`, für Auftragserfassung zusätzlich INSERT auf `log` und SELECT/INSERT auf `semaphor`. Name aus Buchstaben, Ziffern und Unterstrichen, maximal 64 Zeichen |
|
||
| `LEGACY_APP_URL` | Optionale HTTP(S)-Basisadresse der vorhandenen PHP-Anwendung |
|
||
| `SERVER_ADDRESS` | Standard `127.0.0.1` |
|
||
| `PORT` | Standard `8080` |
|
||
| `CORS_ORIGINS` | Standard localhost/127.0.0.1 auf Port 5173 |
|
||
| `FORWARD_HEADERS_STRATEGY` | Standard `native`; Proxy-Header werden durch Tomcat ausgewertet |
|
||
| `SERVER_SERVLET_SESSION_COOKIE_SECURE` | Bei HTTPS in Produktion explizit `true`; ohne Vorgabe vom erkannten Request-Protokoll abhängig |
|
||
| `SESSION_TIMEOUT` | Ablauf bei Inaktivität, Standard `30m` |
|
||
| `MAX_RESULTS` | Abfragegrenze, Standard 500, maximal 5000 |
|
||
|
||
Die Anwendung startet auch ohne erreichbare Datenbank. `/api/health` prüft die Anwendung; `/api/system` meldet die tatsächliche Datenbankerreichbarkeit. Eine fehlgeschlagene Datenabfrage oder Anmeldung bei fehlender DB-Verbindung liefert 503 und niemals erfundene oder leere Ersatzdaten. Die SQL-Kompatibilität muss zusätzlich am tatsächlichen Bestandsschema geprüft werden.
|
||
|
||
## Anmeldung und Umfang
|
||
|
||
Die Anmeldung prüft ausschließlich vorhandene VoTiAn-Konten über `user.usr_account`, `PASSWORD()` und `OLD_PASSWORD()` und speichert die authentifizierte Identität in einer serverseitigen HTTP-Sitzung. Es erfolgt keine Passwortmigration und kein Update. Unterstützt sind vorhandene Zentralenbenutzer (`usr_type=1`) mit Mitarbeiter-, Niederlassungs- und Mandantenzuordnung. Keine Demo-Anmeldung und kein automatisch angelegtes Konto.
|
||
|
||
HTTP Basic ist im Backend deaktiviert. Ein vom Browser für den vorgeschalteten Nginx-Passwortschutz gesendeter `Authorization`-Header wird nicht zur VoTiAn-Anmeldung verwendet. VoTiAn erhält Benutzername und Passwort ausschließlich über `POST /api/login`; das Frontend speichert diese Werte nicht in `sessionStorage` oder `localStorage`.
|
||
|
||
Die Sitzung wird ausschließlich per Cookie `VOTIANSESSION` übertragen (`HttpOnly`, `SameSite=Lax`, Pfad `/`), nicht per URL. Ihre Kennung wechselt bei erfolgreicher Anmeldung. Nach standardmäßig 30 Minuten Inaktivität oder nach `POST /api/logout` ist die Sitzung ungültig. Sitzungen liegen im Backend-Arbeitsspeicher, benötigen keine Datenbanktabellen und gehen beim Neustart verloren. Mehrere Instanzen erfordern Sitzungsaffinität oder eine gemeinsame Sitzungsablage. Der allgemeine Sitzungsbereich ist ein Snapshot bei Anmeldung; Erfassungsanfragen mit `X-Order-Headquarters` prüfen die Niederlassungsfreigaben und Erfassungsrechte zusätzlich aktuell.
|
||
|
||
### Ablauf für API-Clients
|
||
|
||
1. `GET /api/csrf` aufrufen und das dabei gesetzte Sitzungscookie behalten. Die JSON-Antwort enthält `token`, `headerName` und `parameterName`.
|
||
2. `POST /api/login` mit `Content-Type: application/x-www-form-urlencoded`, den Formularfeldern `username` und `password`, dem Sitzungscookie und dem CSRF-Token im angegebenen Header senden. Erfolgreich: `200` mit denselben Benutzerinformationen wie `/api/me`; die Antwort erneuert die Sitzungskennung. Falsche Zugangsdaten liefern `401`, eine nicht verfügbare Datenbank `503`.
|
||
3. Für folgende API-Aufrufe das erneuerte Sitzungscookie verwenden. `GET /api/me` prüft die bestehende Anmeldung; ohne gültige Anmeldung liefert es `401` und keine HTTP-Basic-Aufforderung.
|
||
4. Nach erfolgreicher Anmeldung und nach Abmeldung mit `GET /api/csrf` einen neuen Token beziehen, bevor erneut ein schreibender HTTP-Aufruf erfolgt. Die bisherigen Token werden dabei ungültig. Der Token gehört zur Sitzung und muss bei `POST`, `PUT`, `PATCH` und `DELETE` im angegebenen Header mitgeschickt werden.
|
||
5. Zum Abmelden `POST /api/logout` mit gültigem CSRF-Token und Sitzungscookie senden. Die Antwort ist `204`; Sitzung und Cookie werden gelöscht. `GET /api/logout` meldet nicht ab.
|
||
|
||
Ein fehlender oder falscher CSRF-Token führt zu `403`, auch bei Anmeldung und Abmeldung. Die React-Oberfläche übernimmt diesen Ablauf einschließlich erneuter Anmeldung bei abgelaufener Sitzung. Die CSRF-Token bleiben getrennt vom `HttpOnly`-Sitzungscookie. Hinter einem zusätzlichen Proxy-Passwortschutz bleiben dessen Zugangsdaten für alle Aufrufe notwendig; die VoTiAn-Abmeldung beendet den Proxy-Passwortschutz nicht. Siehe [Spring Security: CSRF für JavaScript-Clients](https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript).
|
||
|
||
### Betrieb hinter Nginx / Nginx Proxy Manager
|
||
|
||
Frontend und API unter derselben HTTPS-Origin bereitstellen und Cookies unverändert weiterleiten. Der Proxy muss `X-Forwarded-Proto: https` setzen und vom Client gelieferte Forwarded-Header ersetzen. Das Backend sollte nur vom vertrauenswürdigen Proxy erreichbar sein. `FORWARD_HEADERS_STRATEGY=native` verwendet Tomcats Proxy-Auswertung einschließlich seiner Vertrauensprüfung für interne Proxyadressen. Falls nötig, diese gezielt mit `SERVER_TOMCAT_REMOTEIP_INTERNAL_PROXIES` anpassen; keine leere Einstellung verwenden, die alle Proxies als vertrauenswürdig behandelt.
|
||
|
||
Im HTTPS-Produktionscontainer `SERVER_SERVLET_SESSION_COOKIE_SECURE=true` setzen. Dann funktionieren Sitzungsanmeldungen nur über HTTPS, auch wenn der interne Weg vom Proxy zum Backend HTTP verwendet. Für den lokalen HTTP-Start die Variable weglassen. Die Vite-Entwicklungsoberfläche verwendet ihren bestehenden `/api`-Proxy, sodass auch dort Oberfläche und API aus Browsersicht dieselbe Origin haben. Ein vorhandener Nginx-Basic-Passwortschutz kann aktiv bleiben; die Systemanmeldung verwendet dafür keinen `Authorization`-Header mehr.
|
||
|
||
Die ursprüngliche Niederlassung `user.hq_id` begrenzt alle Abfragen. Bei Aufträgen gilt die **Erfassungsniederlassung** `job.hq_id`; zusätzliche Dispositions-/Ausführungs-/Vertriebsniederlassungen werden nicht pauschal freigegeben. Kunden werden über `customer.hq_id`, Kuriere über `courier.hq_id` und `user.hq_id` beschränkt. Zusätzliche Rechteprüfungen verwenden die vorhandenen `employee.emp_rights`-Positionen: Kunden 0, Kuriere 1, Auftragslisten 4, Rechnung/CSV 5, Erfassung 7, Lagerbewegungen 14, manuelle Frachtvergütung/Unternehmerverrechnung 38, Überschreitung ihrer Preisgrenze 39. Kunden- und Kurierlogins sind noch nicht portiert. Für die Auftragserfassung ist ein ausdrücklich ausgewählter, je Anfrage berechtigungsgeprüfter Niederlassungswechsel innerhalb des Mandanten verfügbar; andere Listen behalten den ursprünglichen Bereich. Details: [AUFTRAGSERFASSUNG.md](../docs/AUFTRAGSERFASSUNG.md).
|
||
|
||
## API
|
||
|
||
| Route | Verhalten |
|
||
| --- | --- |
|
||
| `GET /api/health` | Öffentlich, Anwendungsbereitschaft |
|
||
| `GET /api/system` | Öffentlich, Verbindung, Funktionsumfang, Legacy-Link, Ergebnisgrenze |
|
||
| `GET /api/csrf` | Öffentlich, CSRF-Token und Header-/Parametername für die aktuelle Sitzung |
|
||
| `POST /api/login` | CSRF-geschützte Anmeldung mit Formularfeldern `username` und `password`; `200` mit Benutzerinformationen |
|
||
| `POST /api/logout` | CSRF-geschützte Abmeldung, Sitzung ungültig machen und Cookie löschen; `204` |
|
||
| `GET /api/me` | Bestehendes Konto, Niederlassung, verfügbare Lesebereiche |
|
||
| `GET /api/orders` | Aufträge mit Kundennamen; Filter `q`, `status`, `customerId` (effektiver Zahler), `courierId`, `from`, `to` |
|
||
| `GET /api/orders/{id}` | Auftrag derselben Erfassungsniederlassung mit Tourstationen |
|
||
| `GET /api/orders/{id}/history` | Vollständige Historie eines aktuellen Auftrags derselben Erfassungsniederlassung; Recht 4; aufsteigend nach Ereigniszeit und Log-ID |
|
||
| `GET /api/orders/export.csv` | Gefilterter UTF-8-Export, zusätzlich Recht 5; verweigert einen unvollständigen Export oberhalb der Ergebnisgrenze |
|
||
| `GET /api/customers`, `GET /api/couriers` | Stammdaten, optionaler Suchparameter `q` |
|
||
| `GET /api/orders/entry/options` | Fahrzeugtypen, Steuern, Wiederholungen; Recht 7 |
|
||
| `GET /api/orders/entry/cost-centers`, `GET /api/orders/entry/cost-centers/{id}/addresses`, `GET /api/orders/entry/vehicles` | HQ-begrenzte Erfassungssuche; Recht 7, unabhängig von Stammdatenrechten |
|
||
| `GET /api/orders/entry/cost-centers/{id}/photo-workflows` | Kundenbezogene Fotooptionen nach HQ-/Kostenstellenprüfung; Recht 7 |
|
||
| `GET /api/orders/entry/features`, `GET /api/orders/entry/articles` | Kartenkonfiguration/Lagerrecht und HQ-begrenzte Artikelsuche; Recht 7 |
|
||
| `GET /api/orders/entry/stocks` | Zugängliche Lagerorte und Artikelbestand für `payerId`, `articleId`, `q`; Rechte 7 und 14 |
|
||
| `POST /api/orders/entry/map/locate`, `POST /api/orders/entry/map/address` | Nur lesende Koordinaten-/Adressauflösung für die eingebettete Karte; Recht 7 und CSRF |
|
||
| `POST /api/orders/quote` | Nur lesende Preisvorschau; Recht 7 und CSRF |
|
||
| `POST /api/orders` | Neuanlage nach erneuter Preisprüfung; 201, identische Wiederholung 200; Recht 7 und CSRF |
|
||
| `GET /api/invoices` | 501, Bestands-Abrechnungsprozess noch nicht portiert |
|
||
| Übrige Schreibmethoden unter `/api/**` | 501 nach erfolgreicher Anmeldung und gültiger CSRF-Prüfung; keine Datenänderung |
|
||
|
||
Listen liefern Arrays. Auftragslisten senden zusätzlich `X-Total-Count` und `X-Result-Limit`. `from`/`to` sind einschließlich, Format `YYYY-MM-DD`, bezogen auf `jb_ordertime`. Nicht belegte API-Felder bleiben `null`, insbesondere Stammdatenversionen, Rechnungs-ID, Lieferfrist, Packstückzahl und Distanz. Die UI soll diese als unbekannt anzeigen.
|
||
|
||
## Fachliche Zuordnung und offene Arbeit
|
||
|
||
- Die Kundenanzeige (`customerId`, `customerName`, `costCenter`), Namenssuche und der Kundenfilter folgen `job.csc_id_payer → costcenter → customer → company`. Bei Barzahlung (`jb_payment=2`) mit positiver `csc_id_payer_cash` wird für diese Anzeige der tatsächliche Barzahler verwendet; `payerCustomerId` behält den Kunden aus `csc_id_payer`, also gegebenenfalls den technischen Systembarzahler.
|
||
- `job.csc_id` bezeichnet die erfassende Kundenkostenstelle und ist bei zentral erfassten Aufträgen regulär `0`. Dieser Bezug bleibt separat als `ordererCustomerId` erhalten. Er darf nicht als allgemeiner Kundenbezug verwendet werden. Quelle: `jobs/job_edit.php:38–48`, `jobs/job_options.php:125–128`, `:1706–1711` und `admin/jb_list.php:1022–1023`.
|
||
- Der Kundenname wird mit jedem sichtbaren Auftrag geliefert; die Anzeige ist damit unabhängig vom Ergebnislimit oder Leserecht der separaten Kundenstammliste. Die Auftragsabfrage bleibt über `job.hq_id` und Recht 4 begrenzt. Nicht zuordenbare Kunden (beispielsweise Kostensplit ohne eindeutigen Zahler) bleiben `null` und werden in der Oberfläche ausdrücklich als nicht zugeordnet angezeigt.
|
||
- Kundenpreis aus `jb_totalprice` (Netto einschließlich Zuschlägen), Kurierpreis aus `jb_cr_price`; keine neu erfundene Tarifierung.
|
||
- Status 0/1/2/8/9 bleiben zusätzlich als `legacyStatus`/`legacyStatusLabel` erhalten. Die UI fasst 8/9 unter Disposition zusammen.
|
||
- `jb_storno` bleibt als `legacyStorno` erhalten. 3/4 sind Gegenbuchungen und werden ausdrücklich als Storno-Gutschriften markiert (`isCreditNote`).
|
||
- Tourrichtung 2=Abholung, 1=Zustellung, 3=beides (`BOTH`), 0=positionsabhängige Legacy-Regel. Maßgeblich sind `job_options.php` und `get_saved_tour.php`; der alte Rechnungstext-Helfer enthält widersprüchlich vertauschte Beschriftungen.
|
||
- Leistungen aus der Bitmaske `jb_service`, wobei `metatype.mt_sort` die Bitposition ist. Fahrzeugtypen werden über `metatype` gelesen.
|
||
- Historie wird beim Öffnen von „Verlauf“ separat über `/api/orders/{id}/history` aus `phoenix_log.log` geladen. `logoperation` liefert Vorgangsbezeichnungen, `user` den Bearbeiter. Die Oberfläche zeigt die neuesten Ereignisse zuerst und unterscheidet Laden, eine erfolgreich gelesene leere Historie und einen Zugriffsfehler mit Wiederholungsmöglichkeit. Das bisherige `history`-Feld in Auftragslisten/-details bleibt leer; maßgeblich ist der eigene Historien-Endpunkt.
|
||
- Die Historienberechtigung folgt `job.hq_id` und Recht 4; Ereignisse anderer Niederlassungen zum selben sichtbaren Auftrag bleiben erhalten. Fehlende/fremde Aufträge liefern 404, Datenbank-/Schemarechtefehler 503. Die Historie wird weder erfunden noch bei einem Fehler durch ein leeres Ergebnis ersetzt. Legacy-Details werden als Text mit bekannten deutschen Feldnamen angezeigt, unbekannte Angaben bleiben erhalten; gespeicherter HTML-Code wird nicht ausgeführt. Das Lesen der Historie erzeugt keine Datenbankschreibzugriffe. Die Erfassung ergänzt nach dem Commit eigene Ereignisse.
|
||
- Abrechnung im Bestand ist keine nachgewiesene einfache Invoice-CRUD-Tabelle. Belege, Gegenbuchungen, Zahlungsstatus, Auftragsänderung und Fahrerkommunikation bleiben bis zur vollständigen Portierung im vorhandenen PHP-System. Optional verlinkt die React-Oberfläche diese Module.
|
||
- Archivtabellen und Mehrniederlassungsrechte sind noch nicht eingebunden. Die bestehende Anwendung bleibt für diese Funktionen erforderlich.
|
||
|
||
SQL-/Mapping-Quellen im vorhandenen Code: `include/mcglobal.inc.php`, `include/dbglobal.inc.php`, `include/caglobal.inc.php`, `admin/login.php`, `admin/menu.php`, `jobs/job_options.php`, `admin/jb_list.php` und `invoice/inv.inc.php`.
|
||
|
||
## Prüfung
|
||
|
||
`./mvnw test` führt Tests ohne Bestandszugriff aus: bestehende Anmeldung und Rechte, Sitzungs- und CSRF-Verhalten, HQ-Abgrenzung, SQL-Parameterbindung, Nichtverfügbarkeit, Abgrenzung der Erfassungs-Schreibtransaktionen, serverseitige Vorbereitung der Lesetransaktion, Transaktions-/Fehlerverhalten, Schemadiagnose, Legacy-Status/Preis-/Tour-Mapping einschließlich JDBC-Boolean-Flags und CSV-Schutz. Normale Maven-Builds und Unit-Tests laden `.env` nicht. Ein Architekturtest prüft die kompilierten Java-Abhängigkeiten und Python-Launcher auf direkte Datenbankzugriffe außerhalb des Services. Nur dessen eigener Unit-Test darf JDBC mocken. Es wird keine Testdatenbank angelegt und keine Bestandsdatenbank verändert. Erfassungs-, Preis- und Audit-Tests verwenden gemockte Datenzugriffe; sie belegen keine produktive Ende-zu-Ende-Verarbeitung.
|
||
|
||
Mit `VOTIAN_VERIFY_EXISTING_DB=true` und den konfigurierten `DB_URL`, `DB_USER`, `DB_PASSWORD` wird zusätzlich `LegacyDatabaseReadIntegrationTest` ausgeführt. Er prüft die drei Listen-APIs mit echten SELECT-Abfragen über MockMvc, die numerische JDBC-Abbildung von `TINYINT(1)` und den Historien-Endpunkt gegen das konfigurierte Protokollschema. Für die Historie muss einer der neuesten 100 aktuellen Aufträge Protokolleinträge besitzen; geprüft werden Anzahl und Antwortformat ohne Ausgabe der Protokollinhalte. Die Authentifizierung wird nur in diesem Test simuliert; die echte Anmeldung wird damit nicht getestet. Die gewählte Niederlassung muss vorhandene Aufträge, Kunden und Kuriere enthalten. Ohne ausdrückliche Aktivierung bleibt dieser Test deaktiviert.
|
||
|
||
Für diesen bewusst aktivierten Integrationstest mit der lokalen `.env` im Verzeichnis `backend/` ausführen:
|
||
|
||
```sh
|
||
VOTIAN_VERIFY_EXISTING_DB=true python3 ../scripts/env_config.py ./mvnw test
|
||
```
|
||
|
||
Der Integrationstest vergleicht außerdem die Kundenprojektion der neuesten 100 Aufträge mit unabhängigen Abfragen der Zahler-, Barzahler- und Erfassungskundenbezüge. Auch hierbei enthalten Testausgaben keine Kunden- oder Auftragsinhalte.
|
||
|
||
### Vertriebsfunktionen und SMTP
|
||
|
||
`/api/crm` stellt Kontakte (Metafeldkategorie 1), Kundenstatusauswertung und Motivation-Counter bereit. Kontaktänderungen benötigen InnoDB sowie `LOCK TABLES` auf den vier vorhandenen Metafeldtabellen, um die ID-Vergabe ohne Schemaänderung mit dem PHP-Bestand zu serialisieren. Counter-Ziele nutzen die bestehenden `MOTIVATIONCOUNTER_YYYY_MM`-Parameter und das vorhandene `MOTIVATIONCOUNTER_PASSWORD` der jeweiligen Niederlassung.
|
||
|
||
Termin-E-Mails werden separat über Vorschau und explizite Sendebestätigung ausgelöst. Konfiguration aus `.env`: `SMTP_HOST`, `SMTP_PORT`, `SMTP_USERNAME`, `SMTP_PASSWORD`, `SMTP_STARTTLS`, `SMTP_SSL`, `SMTP_FROM`. Leeres `SMTP_FROM` verwendet die Benutzer-Mailadresse. Kein Versand beim bloßen Speichern, keine automatische Wiederholung nach unklarer Zustellung. SMTP wurde nur gegen einen isolierten lokalen Testserver geprüft.
|
||
|
||
Optionaler Kontakt-Integrationstest: ausschließlich eine wegwerfbare lokale MariaDB mit Datenbank `crm_qa`, Port 33079 und Testkonto `root`/`isolated-test-only` bereitstellen; `CRM_INTEGRATION_URL=jdbc:mariadb://127.0.0.1:33079/crm_qa ./mvnw test`. Der Test ersetzt ausschließlich vier Metafeldtabellen dieser Testdatenbank; nie auf den Bestand richten.
|