# 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`).
