procurex-app
Spring-Boot-Bootstrap-Modul (Confluence Seite 09): bindet alle Bounded-Context-Module unter
com.procurex.* zu einem einzigen deploybaren Artefakt (modularer Monolith).
Voraussetzung: enterprise-infrastructure
enterprise-infrastructure (C:\Entwicklung\lernen\enterprise-workspace\shared\enterprise-infrastructure) muss vorher mit dem Profil
postgres laufen:
docker compose --profile postgres up -d postgres
Datenbank procurex_db + eigener Benutzer procurex_user sind in
shared/enterprise-infrastructure/postgres/init-project-databases.sh registriert (Konvention: ein Eintrag pro
angeschlossenem Projekt, siehe shared/enterprise-infrastructure/README.md). Läuft die Postgres-Instanz bereits
mit bestehenden Daten (Init-Skript läuft nur beim allerersten Start eines leeren Volumes), wird
die Datenbank stattdessen einmalig direkt angelegt:
docker exec -e PGPASSWORD=admin shared-postgres psql --username admin --dbname postgres \
-c "CREATE USER procurex_user WITH PASSWORD 'aendern-vor-erstem-start-procurex';" \
-c "CREATE DATABASE procurex_db OWNER procurex_user;" \
-c "REVOKE ALL ON DATABASE procurex_db FROM PUBLIC;" \
-c "GRANT ALL PRIVILEGES ON DATABASE procurex_db TO procurex_user;"
Voraussetzung: Keycloak (PRX-19, Umsetzung)
enterprise-infrastructure mit dem Profil keycloak (braucht zusaetzlich postgres):
docker compose --profile postgres --profile keycloak up -d keycloak
Realm procurex, Clients und Rollen einmalig anlegen — siehe ../keycloak/README.md
(Realm-Export dort zum Importieren, oder Admin-Konsole http://keycloak.localhost, admin/admin).
Starten – lokal mit Maven
Vor dem Start muss PROCUREX_DB_PASSWORD gesetzt sein. Es gibt absichtlich kein
changeme-Fallback mehr; ein fehlendes Datenbank-Secret führt zu einem sichtbaren
Konfigurationsfehler statt zu einem scheinbar erfolgreichen unsicheren Start.
mvn -pl procurex-app -am spring-boot:run
Nutzt per Default den Host-Port 5433 von enterprise-infrastructure (siehe application.yml).
Umgebungsvariablen: PROCUREX_DB_HOST, PROCUREX_DB_PORT, PROCUREX_DB_NAME, PROCUREX_DB_USER,
PROCUREX_DB_PASSWORD, PROCUREX_CORS_ALLOWED_ORIGINS, PROCUREX_OIDC_ISSUER_URI,
PROCUREX_OIDC_JWK_SET_URI.
Security (PRX-19, Umsetzung)
procurex-app ist OAuth2 Resource Server gegen Keycloak (SecurityConfig). /actuator/health/**
und /actuator/prometheus sind offen, alle /api/**-Endpunkte brauchen ein gueltiges JWT;
schreibende Endpunkte zusaetzlich die passende Rolle (ROLE_SUPPLIER_ADMIN, ROLE_BUYER,
ROLE_APPROVER, ROLE_RECEIVING, ROLE_FINANCE — ROLE_ADMIN darf ueberall). Rollen kommen aus
dem realm_access.roles-Claim des Access Tokens. Live verifiziert (Token via Direct-Grant-Testlauf,
siehe Commit-Historie): korrekte Rolle → Erfolg/fachlicher Fehler, falsche Rolle → 403, kein
Token → 401.
Gefundener und behobener Bug dabei: @PathVariable-Parameter ohne expliziten Namen scheiterten
zur Laufzeit (IllegalArgumentException), weil das Root-POM nicht von spring-boot-starter-parent
erbt und daher -parameters nicht automatisch beim Compile gesetzt wird — behoben über
maven.compiler.parameters=true im Root-pom.xml. War vorher unbemerkt, weil die Unit-Tests die
Controller-Schicht nicht durchlaufen (nur Domain/Application-Service, siehe Root-README).
Voraussetzung: Kafka (PRX-17, Umsetzung)
enterprise-infrastructure mit dem Profil kafka:
docker compose --profile kafka up -d kafka kafka-ui
Events (PRX-17, Umsetzung)
Transactional-Outbox-Pattern (Confluence Seite 10): jede fachliche Aenderung schreibt in
derselben Transaktion einen Outbox-Eintrag ins eigene Schema (<schema>.outbox_event); ein
@Scheduled-Relay pro Modul (com.procurex.common.outbox.AbstractOutboxRelay, alle 2s) sendet
unveroeffentlichte Eintraege an Kafka und markiert sie danach als published. Live verifiziert
(Nachricht via kafka-console-consumer gegenlesen).
| Topic | Modul | Ausgeloest durch |
|---|---|---|
procurex.ordering.order-submitted.v1 |
procurex-ordering | PRX-2 Warenkorb absenden |
procurex.approval.order-approved.v1 |
procurex-approval | PRX-5 Genehmigung |
procurex.approval.order-rejected.v1 |
procurex-approval | PRX-5 Ablehnung |
procurex.receiving.goods-received.v1 |
procurex-receiving | PRX-10 Wareneingang |
procurex.invoicing.invoice-matched.v1 |
procurex-invoicing | PRX-11 Rechnungspruefung ohne Abweichung |
procurex.invoicing.invoice-discrepancy-flagged.v1 |
procurex-invoicing | PRX-9 Mengenabweichung |
Gefundener und behobener Bug dabei: Alle vier Module hatten anfangs eine Klasse namens
OutboxEvent/OutboxEventRepository im jeweils eigenen Package — Spring registriert Data-JPA-
Repository-Beans aber standardmaessig nach einfachem Klassennamen (nicht vollqualifiziert), und
Hibernate ebenso den Entity-Namen, was zu Bean-Kollisionen beim Start fuehrte
(BeanDefinitionOverrideException). Behoben durch eindeutige Namen pro Modul
(OrderingOutboxEvent, ApprovalOutboxEvent, ReceivingOutboxEvent, InvoicingOutboxEvent).
Erster Konsument (PRX-34, Umsetzung): procurex-audit (AuditLogConsumer) schreibt alle
sechs Topics unveraendert in audit.audit_log_entry — rein additiv, beeinflusst keine fachlichen
Ablaeufe. Idempotenz: AbstractOutboxRelay haengt die eigene Outbox-Event-ID als Kafka-Header
event-id an (nicht im Payload, der bleibt reine fachliche DTO); ein eindeutiger DB-Index auf
event_id faengt doppelte Zustellung bei Redelivery ab, unabhaengig von Kafka-Offsets. Live
verifiziert: Testnachricht mit event-id-Header manuell publiziert → landet in
audit_log_entry; dieselbe Nachricht erneut publiziert → weiterhin genau 1 Zeile (Dedup
bestaetigt). Aeltere, vor diesem Feature publizierte Nachrichten ohne den Header werden erkannt
und uebersprungen (geloggt als WARN), nicht fehlerhaft verarbeitet.
Im Test deaktiviert (procurex.audit.consumer-enabled: false, siehe
procurex-app/src/test/resources/application.yml) aus demselben Grund wie das Outbox-Relay:
Tests sollen nicht von einem erreichbaren Kafka-Broker abhaengen.
Weiterhin keine DLQ-Umsetzung (nur die Namenskonvention <topic>.dlq ist in Confluence Seite 10
dokumentiert) — bei diesem einen, rein additiven Consumer bisher nicht notwendig.
Integrationstests (PRX-8, PRX-24, Umsetzung)
Drei Testklassen unter src/test/java/com/procurex/app decken echtes DB-Verhalten ab, das reine
Mockito-Unit-Tests in den Einzelmodulen strukturell nicht pruefen koennen (dort existiert keine
echte Datenbank/Transaktion):
| Testklasse | Prueft |
|---|---|
ApprovalConcurrencyIntegrationTest |
PRX-8 Optimistic Locking: zwei gleichzeitige Genehmigungsentscheidungen auf derselben Bestellung — genau eine gewinnt, die andere bekommt ConflictException |
UniqueConstraintIntegrationTest |
DB-Unique-Constraints (Supplier taxId, Approval order_id) |
ProcureToPayFlowIntegrationTest |
Kompletter Procure-to-Pay-Fluss (PRX-2 bis PRX-11) ueber echte, Spring-verwaltete Services statt Mocks — happy path (MATCHED) und Mengenabweichung (DISCREPANCY) |
IntegrationTestBase startet dafuer per Testcontainers (@ServiceConnection) je Testklasse einen
echten PostgreSQL-16-Container. @DirtiesContext(AFTER_CLASS) verhindert, dass Spring nach dem
Container-Stopp eine veraltete dynamische JDBC-URL aus dem Context-Cache wiederverwendet.
Lokaler Lauf mit Podman: Docker Desktop 4.84 lieferte dem verwendeten Docker-Java-Client eine
unvollstaendige 400-Antwort. Die stabile lokale Loesung ist Podman 6.x mit einer gestarteten
Podman-Machine und deren Docker-kompatibler Named Pipe. Testcontainers ist dafuer auf 2.0.4
aktualisiert. enterprise-infrastructure wird von diesen Tests bewusst nicht verwendet; es bleibt der Laufzeit-
Stack fuer Compose und die manuellen Integrationschecks.
podman machine start podman-machine-default
$env:TESTCONTAINERS_RYUK_DISABLED = 'true'
mvn -pl procurex-app -am test `
"-Dprocurex.integration=true" `
"-Ddocker.host=npipe:////./pipe/docker_engine"
In PowerShell muessen die Properties wegen des Punkts im Namen quotiert werden. Einzelne Tests
koennen mit -Dtest=ApprovalConcurrencyIntegrationTest ausgewaehlt werden. Ohne die Eigenschaft
procurex.integration aktiviert das fast-tests-Profil; damit bleiben die teuren
Testcontainers-Laeufe im normalen Quality-Gate ausgeschlossen.
Contract-Tests (PRX-35, Umsetzung)
Spring Cloud Contract Verifier generiert aus Contract-DSL-Dateien unter
src/test/resources/contracts/<modul>/*.groovy JUnit-5-Tests, die den echten Controller (mit
gemocktem Service, @WebMvcTest) gegen die im Contract festgelegte Anfrage/Antwort pruefen —
anders als die Testcontainers-Integrationstests oben braucht das keine Datenbank und laeuft
daher unabhaengig von der obigen Docker-Desktop-Einschraenkung, auch in der Jenkins-Pipeline.
| Contract | Prueft |
|---|---|
contracts/supplier/shouldCreateSupplier.groovy |
POST /api/v1/suppliers liefert 201 mit der erwarteten SupplierResponse-Form |
contracts/supplier/shouldRejectSupplierWithoutName.groovy |
Fehlender Pflichtname liefert 400 als RFC-7807 Problem Detail mit errors-Liste |
contracts/requisition/shouldSubmitCart.groovy |
POST /api/v1/cart/submit liefert 201 mit orderId/Status PENDING_APPROVAL |
Je eine Basisklasse pro benoetigter Rolle (com.procurex.app.contracts.SupplierAdminContractBase,
BuyerContractBase) simuliert die von SecurityConfig verlangte Rolle ueber
SecurityMockMvcRequestPostProcessors.jwt() (kein echtes Keycloak-Token noetig, der JwtDecoder
wird nur fuer die Bean-Verdrahtung gemockt, nie aufgerufen). Jede Basisklasse bringt ihre eigene,
schmale @SpringBootConfiguration mit (scannt nur das jeweilige Modul-Package plus
com.procurex.common.web fuer den GlobalExceptionHandler) — ein Scan von ganz com.procurex
wuerde auch fremde Controller und ProcurexApplications @EnableJpaRepositories mitziehen und
wieder einen echten Datenbankzugriff erzwingen.
mvn -pl procurex-app -am test
CI (PRX-32, Umsetzung)
Jenkinsfile im Repo-Root, laeuft im geteilten Jenkins aus enterprise-infrastructure (jenkins.localhost,
Job procurex-pipeline, angelegt per Configuration-as-Code in
shared/enterprise-infrastructure/jenkins/casc.yaml — zweiter angeschlossener Job neben dem eines anderen Projekts,
siehe dortige Kommentare zum Multi-Projekt-Muster). Vier Stufen:
- Backend: Build & Test —
mvn testreactor-weit;procurex-apps drei Testcontainers-
Integrationstests sind dabei über das standardmäßig aktive fast-tests-Profil ausgenommen (siehe
oben), die Contract-Tests aus PRX-35 laufen normal mit.
- Frontend: Install & Test —
npm ci+ng test(Vitest+jsdom), in einem eigenen
node:22-alpine-Docker-Agent statt dem am Jenkins-Controller fest installierten Node 20 (der
ist auf das andere angeschlossene Projekt abgestimmt — eigener Agent nur fuer diese Stufen
vermeidet, das geteilte Jenkins-Image fuer alle anzupassen).
- Frontend: Build —
ng build(production). - Docker Images — baut
procurex-app- undprocurex-frontend-Images lokal im
Docker-Daemon des Jenkins-Hosts (ueber den in shared/enterprise-infrastructure/jenkins/docker-compose.yml
dokumentierten Socket-Mount). Kein Registry-Push, kein Deployment — bewusst nicht Teil dieses
Tickets (siehe PRX-22 fuer Kubernetes/Helm). Der Docker-CLI dort hat keinen buildx
installiert (nur der Legacy-Builder) — procurex-app/Dockerfile verzichtet deshalb bewusst auf
BuildKit-spezifische Features wie --mount=type=cache, um auf jedem Docker-Setup zu bauen.
Voraussetzung: procurex-platform ist ein privates GitHub-Repo, der Checkout braucht daher ein
Jenkins-Credential (github-procurex-pat, Typ „Username with password“, Passwort = Fine-grained
GitHub PAT mit Contents: Read-only nur fuer dieses Repo) — einmalig angelegt, der Wert steht
bewusst nicht in casc.yaml (gleiches Prinzip wie bei den dortigen KUBECONFIG-Credentials).
Starten – Docker (PRX-21)
Siehe ../docker-compose.yml im Repo-Root (bindet procurex-app und procurex-frontend über
das externe proxy-Netzwerk an Traefik an). Kurzfassung:
cd ..\..\shared\enterprise-infrastructure
docker compose --profile postgres up -d postgres
docker compose up -d traefik
cd ..\..\projects\procurex-platform
cp .env.example .env # Passwort ggf. anpassen, muss zum enterprise-infrastructure-Eintrag passen
docker compose up -d --build
Danach erreichbar unter http://procurex-api.localhost (Backend) und http://procurex.localhost
(Frontend) – getestet inkl. Flyway-Migrationen, /actuator/health und CORS-Preflight von
procurex.localhost aus.
Endpunkte
/actuator/health,/actuator/prometheus(Confluence Seite 14)- Alle
/api/v1/*-Endpunkte der einzelnen Module, siehe deren jeweiliges README.md