procurex-approval
Bounded Context: Genehmigung — PRX-5 ("Als Genehmiger möchte ich Bestellungen entscheiden") und
PRX-8 ("Doppelte Genehmigungsentscheidung verhindern").
Funktionsweise
ApprovalService.decide(...) ruft zunächst OrderApprovalPort in procurex-ordering auf (dort liegt
die eigentliche Einmaligkeits-Garantie über Status + Optimistic Locking) und protokolliert die
Entscheidung anschließend als ApprovalRequest — abgesichert durch einen **eindeutigen Index auf
order_id** in der Datenbank als zweite Sicherungslinie gegen Race-Conditions (PRX-8). Ein Verstoß
dagegen wird als ConflictException (409) gemeldet.
Akzeptanzkriterien (PRX-5)
Genehmigung und Ablehnung sind einmalig, begründet (Pflichtfeld reason) und über den Audit-Datensatz
nachvollziehbar.
Fachliche Genehmigungsregeln (FR-005/FR-010, seit 2026-08-17)
Zusätzlich zur rein technischen Rollenprüfung am Endpunkt (ROLE_APPROVER/ROLE_ADMIN, siehe
SecurityConfig in procurex-app) prüft ApprovalService.decide(...) zwei fachliche Regeln,
bevor die Entscheidung überhaupt an procurex-ordering weitergereicht wird — beide lösen
ForbiddenException (HTTP 403) aus, ROLE_ADMIN ist von beiden ausgenommen (globaler Override):
- FR-005 Betragsschwelle: Bestellungen oberhalb
procurex.approval.high-value-threshold
(Default 5000.00, siehe application.yml) dürfen nur mit ROLE_ADMIN entschieden werden. Kein
Authentication-Kontext gilt als "keine ausreichende Rolle" (fail closed).
- FR-010 Organisationsgrenze: ein Genehmiger darf nur Bestellungen der eigenen Kostenstelle
entscheiden (cost_center-JWT-Claim vs. PurchaseOrder.costCenter). **Bewusst nicht
fail-closed:** fehlt der Claim (weil der nötige Keycloak-Attribut-Rollout noch nicht auf der
jeweiligen Instanz nachgezogen wurde, siehe keycloak/README.md), wird die Regel nicht
durchgesetzt statt jede Entscheidung zu blockieren.
REST-Endpunkte
| Methode | Pfad | Zweck |
|---|---|---|
| POST | /api/v1/orders/{orderId}/approval/approve |
Bestellung genehmigen (Body: {"reason": "..."}) |
| POST | /api/v1/orders/{orderId}/approval/reject |
Bestellung ablehnen (Body: {"reason": "..."}) |
Daten
Eigenes PostgreSQL-Schema approval (siehe src/main/resources/db/migration/V5__approval_schema.sql).