PROCUREX · Statische HTML-Dokumentation

procurex-app

Aus procurex-app\README.md konvertiert – vollständig lokal lesbar.

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_FINANCEROLE_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:

  1. Backend: Build & Testmvn test reactor-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.

  1. Frontend: Install & Testnpm 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).

  1. Frontend: Buildng build (production).
  2. Docker Images — baut procurex-app- und procurex-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

⌂ Cockpit