CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Failing after 1m6s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Successful in 3s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Successful in 4s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Failing after 23s
CI / Web (dms-browser) (push) Failing after 1m31s
CI / Web (pdf-viewer) (push) Failing after 1m3s
CI / Web (shl) (push) Failing after 14s
Supply-Chain-Scan (Go) / govulncheck (push) Failing after 4m11s
Workflow-Schnittstellen-Vertragstests / contract-tests (push) Failing after 41s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Successful in 7s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (pull_request) Successful in 8s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Failing after 30s
CI / Web (dms-browser) (pull_request) Failing after 1m24s
CI / Web (pdf-viewer) (pull_request) Failing after 57s
CI / Web (shl) (pull_request) Failing after 11s
CI / Core-Modul (Build/Vet/Test) (pull_request) Failing after 11m29s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Successful in 10m30s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Failing after 3m26s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Failing after 1m57s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Successful in 10m25s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Successful in 9m47s
Workflow-Schnittstellen-Vertragstests / contract-tests (pull_request) Failing after 57s
Workflow-Pflichttest-Gate / pflichttest-gate (pull_request) Failing after 1m13s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Failing after 3s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Failing after 3s
DB-Integrationstests / db-tests (pull_request) Canceled after 0s
Das Pflichttest-Gate von workflow verweist nicht mehr auf die geloeschten Pakete (common hat ein eigenes Gate). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
155 lines
4.8 KiB
Go
155 lines
4.8 KiB
Go
// Package routingengine implementiert RTE-02: fuehrt das Regelwerk aus
|
|
// RTE-01 gegen eingehende Objekte aus. Erste zutreffende Regel nach
|
|
// Prioritaet gewinnt, sonst landet das Objekt im Klaerungskorb statt
|
|
// verloren zu gehen.
|
|
package routingengine
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/routingrule"
|
|
)
|
|
|
|
// Decision ist das Ergebnis einer Ausfuehrung: entweder eine passende Regel
|
|
// mit ihrer Aktion, oder keine Regel (Klaerungskorb).
|
|
type Decision struct {
|
|
Matched bool
|
|
RuleID string
|
|
RuleName string
|
|
Action routingrule.Action
|
|
InBasket bool
|
|
BasketID string
|
|
}
|
|
|
|
type Engine struct {
|
|
rules *routingrule.Store
|
|
pool *pgxpool.Pool
|
|
}
|
|
|
|
func NewEngine(rules *routingrule.Store, pool *pgxpool.Pool) *Engine {
|
|
return &Engine{rules: rules, pool: pool}
|
|
}
|
|
|
|
// Execute wertet das Regelwerk live gegen object aus (Pruefung 3: eine
|
|
// Regeländerung wirkt beim naechsten Execute-Aufruf, da ListEnabled bei
|
|
// jedem Aufruf frisch aus der Datenbank liest — kein In-Memory-Cache, also
|
|
// kein Neustart noetig). objectType/objectID identifizieren das Objekt fuer
|
|
// Protokoll und Klaerungskorb, unabhaengig vom aufrufenden Modul.
|
|
func (e *Engine) Execute(ctx context.Context, objectType, objectID string, object map[string]any) (Decision, error) {
|
|
rules, err := e.rules.ListEnabled(ctx)
|
|
if err != nil {
|
|
return Decision{}, fmt.Errorf("routingengine: regeln laden: %w", err)
|
|
}
|
|
|
|
for _, r := range rules {
|
|
if !r.Matches(object) {
|
|
continue
|
|
}
|
|
if len(r.Actions) == 0 {
|
|
continue
|
|
}
|
|
action := r.Actions[0]
|
|
if err := e.recordDecision(ctx, objectType, objectID, &r.ID, action); err != nil {
|
|
return Decision{}, err
|
|
}
|
|
return Decision{Matched: true, RuleID: r.ID, RuleName: r.Name, Action: action}, nil
|
|
}
|
|
|
|
// Akzeptanzkriterium 1: keine zutreffende Regel -> Klaerungskorb statt
|
|
// Verlust des Objekts.
|
|
basketID, err := e.addToBasket(ctx, objectType, objectID, "keine zutreffende regel")
|
|
if err != nil {
|
|
return Decision{}, err
|
|
}
|
|
if err := e.recordDecision(ctx, objectType, objectID, nil, routingrule.Action{}); err != nil {
|
|
return Decision{}, err
|
|
}
|
|
return Decision{Matched: false, InBasket: true, BasketID: basketID}, nil
|
|
}
|
|
|
|
// recordDecision protokolliert JEDE Weiterleitung nachvollziehbar
|
|
// (Akzeptanzkriterium 2), auch die ohne Treffer.
|
|
func (e *Engine) recordDecision(ctx context.Context, objectType, objectID string, ruleID *string, action routingrule.Action) error {
|
|
var actionJSON []byte
|
|
if ruleID != nil {
|
|
b, err := json.Marshal(action)
|
|
if err != nil {
|
|
return fmt.Errorf("routingengine: aktion kodieren: %w", err)
|
|
}
|
|
actionJSON = b
|
|
}
|
|
_, err := e.pool.Exec(ctx, `
|
|
INSERT INTO routing_decisions (object_type, object_id, matched_rule_id, matched_action)
|
|
VALUES ($1, $2, $3, $4)
|
|
`, objectType, objectID, ruleID, actionJSON)
|
|
if err != nil {
|
|
return fmt.Errorf("routingengine: entscheidung protokollieren: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *Engine) addToBasket(ctx context.Context, objectType, objectID, reason string) (string, error) {
|
|
var id string
|
|
err := e.pool.QueryRow(ctx, `
|
|
INSERT INTO routing_clarification_basket (object_type, object_id, reason)
|
|
VALUES ($1, $2, $3) RETURNING id
|
|
`, objectType, objectID, reason).Scan(&id)
|
|
if err != nil {
|
|
return "", fmt.Errorf("routingengine: klaerungskorb eintragen: %w", err)
|
|
}
|
|
return id, nil
|
|
}
|
|
|
|
// UnresolvedBasketEntries liefert alle offenen Klaerungskorb-Eintraege
|
|
// (Pruefung 2: kein Testobjekt geht verloren -- nachweisbar ueber diese
|
|
// Liste).
|
|
func (e *Engine) UnresolvedBasketEntries(ctx context.Context) ([]BasketEntry, error) {
|
|
rows, err := e.pool.Query(ctx, `
|
|
SELECT id, object_type, object_id, reason, created_at
|
|
FROM routing_clarification_basket
|
|
WHERE resolved_at IS NULL
|
|
ORDER BY created_at ASC
|
|
`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("routingengine: klaerungskorb lesen: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []BasketEntry
|
|
for rows.Next() {
|
|
var b BasketEntry
|
|
if err := rows.Scan(&b.ID, &b.ObjectType, &b.ObjectID, &b.Reason, &b.CreatedAt); err != nil {
|
|
return nil, fmt.Errorf("routingengine: zeile lesen: %w", err)
|
|
}
|
|
out = append(out, b)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// ResolveBasketEntry markiert einen Klaerungskorb-Eintrag als aufgeloest
|
|
// (z. B. nachdem manuell weitergeleitet wurde).
|
|
func (e *Engine) ResolveBasketEntry(ctx context.Context, id string) error {
|
|
res, err := e.pool.Exec(ctx, `UPDATE routing_clarification_basket SET resolved_at = now() WHERE id = $1 AND resolved_at IS NULL`, id)
|
|
if err != nil {
|
|
return fmt.Errorf("routingengine: klaerungskorb aufloesen: %w", err)
|
|
}
|
|
if res.RowsAffected() == 0 {
|
|
return pgx.ErrNoRows
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type BasketEntry struct {
|
|
ID string
|
|
ObjectType string
|
|
ObjectID string
|
|
Reason string
|
|
CreatedAt time.Time
|
|
}
|