Sprint 0: FastAPI-Skeleton, vollständiges DB-Schema (Alembic), Auth (JWT/bcrypt), Rollen-Dependency, CI
CI / backend-tests (push) Failing after 1s

- Vollständiges Ziel-Schema aus Prompt 20 als initiale Migration (inkl. Karte-13-Vorbereitung: UUID-PKs, systemknoten)
- Seed-Migration für Hauptserver-Datensatz (Sprintplan E6)
- JWT-Login + /auth/me, require_roles-Dependency (Prompt 05 Berechtigungsmatrix)
- Tests: health, login/me, Rollen-Ablehnung/-Zulassung (S0-Abnahmekriterien)
- Gitea-Actions-CI: install -> migrate -> pytest mit Coverage-Gate 50%

Nur Code/Config erzeugt, nicht lokal installiert oder ausgeführt (Deployment-Regel).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
This commit is contained in:
2026-09-03 22:32:54 +02:00
co-authored by Claude Sonnet 5
parent dbc30356f6
commit 48b6d6a5be
34 changed files with 1006 additions and 0 deletions
@@ -0,0 +1,291 @@
"""Initiales Schema (Prompt 20 / ergebnisse/20_datenbank_schema.md)
Revision ID: 0001_initial_schema
Revises:
Create Date: 2026-09-03
Vollständiges Ziel-Schema aus Prompt 20 (inkl. Karte-13-Vorbereitung: UUID auf
sync-relevanten Tabellen, systemknoten). Sprint 0 legt das komplette Schema an,
auch wenn erst spätere Sprints die zugehörigen Endpunkte/Business-Logik bauen
so entfällt ein späterer Schema-Umbau (Designziel, Prompt 06/19).
"""
from typing import Sequence, Union
from alembic import op
revision: str = "0001_initial_schema"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
UPGRADE_SQL = """
-- 1. Stammdaten
CREATE TABLE bereich (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
beschreibung TEXT
);
CREATE TABLE kategorie (
id SERIAL PRIMARY KEY,
bereich_id INTEGER NOT NULL REFERENCES bereich(id),
name TEXT NOT NULL,
ueberkategorie_id INTEGER REFERENCES kategorie(id),
UNIQUE (bereich_id, name)
);
CREATE TABLE standort (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
adresse TEXT
);
CREATE TABLE objekttyp (
id SERIAL PRIMARY KEY,
bereich_id INTEGER NOT NULL REFERENCES bereich(id),
kategorie_id INTEGER REFERENCES kategorie(id),
name TEXT NOT NULL,
UNIQUE (bereich_id, name)
);
CREATE TYPE materialtyp AS ENUM ('standard', 'ablauf_charge', 'geraet_sn');
CREATE TABLE material (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
artikelnummer TEXT,
einheit TEXT NOT NULL,
materialtyp materialtyp NOT NULL,
kategorie_id INTEGER REFERENCES kategorie(id),
hersteller TEXT,
beschreibung TEXT,
code TEXT UNIQUE,
warnzeitraum_tage INTEGER,
aktiv BOOLEAN NOT NULL DEFAULT TRUE
);
-- 2. Vorlagen
CREATE TYPE vorlage_status AS ENUM ('aktiv', 'veraltet');
CREATE TABLE beladungsvorlage (
id SERIAL PRIMARY KEY,
objekttyp_id INTEGER NOT NULL REFERENCES objekttyp(id),
name TEXT NOT NULL,
version INTEGER NOT NULL,
gueltig_ab TIMESTAMPTZ NOT NULL DEFAULT now(),
status vorlage_status NOT NULL DEFAULT 'aktiv',
UNIQUE (objekttyp_id, name, version)
);
CREATE TABLE vorlagenposition (
id SERIAL PRIMARY KEY,
vorlage_id INTEGER NOT NULL REFERENCES beladungsvorlage(id),
material_id INTEGER NOT NULL REFERENCES material(id),
fach TEXT,
sollmenge NUMERIC NOT NULL,
UNIQUE (vorlage_id, material_id)
);
-- 3. Objekte
CREATE TYPE knoten_typ AS ENUM ('haupt', 'satellit');
CREATE TABLE systemknoten (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
typ knoten_typ NOT NULL DEFAULT 'satellit'
);
CREATE TYPE objekt_status AS ENUM ('aktiv', 'ausser_dienst');
CREATE TABLE objekt (
id SERIAL PRIMARY KEY,
code TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
objekttyp_id INTEGER NOT NULL REFERENCES objekttyp(id),
vorlage_id INTEGER REFERENCES beladungsvorlage(id),
standort_id INTEGER NOT NULL REFERENCES standort(id),
status objekt_status NOT NULL DEFAULT 'aktiv',
zustaendiger_server_id INTEGER NOT NULL REFERENCES systemknoten(id)
);
CREATE TYPE objektposition_status AS ENUM ('aktiv', 'entfernt');
CREATE TABLE objektposition (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
objekt_id INTEGER NOT NULL REFERENCES objekt(id),
material_id INTEGER NOT NULL REFERENCES material(id),
sollmenge_override NUMERIC,
ist_status objektposition_status NOT NULL DEFAULT 'aktiv',
istmenge NUMERIC NOT NULL DEFAULT 0,
seriennummer TEXT,
ablaufdatum DATE,
chargennummer TEXT,
zuletzt_geaendert_am TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (objekt_id, material_id)
);
-- 4. Zuständigkeiten, Benutzer, Rollen
CREATE TABLE benutzer (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
login TEXT NOT NULL UNIQUE,
passwort_hash TEXT NOT NULL,
aktiv BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TYPE rolle_typ AS ENUM ('mitarbeiter', 'materialverantwortlicher', 'leitungsverantwortlicher', 'administration');
CREATE TABLE benutzer_rolle (
benutzer_id INTEGER NOT NULL REFERENCES benutzer(id),
rolle rolle_typ NOT NULL,
PRIMARY KEY (benutzer_id, rolle)
);
CREATE TABLE zustaendigkeit (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
benutzer_id INTEGER NOT NULL REFERENCES benutzer(id),
standort_id INTEGER REFERENCES standort(id),
objekt_id INTEGER REFERENCES objekt(id),
CHECK (standort_id IS NOT NULL OR objekt_id IS NOT NULL)
);
CREATE TABLE kontrollverantwortung (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
objekt_id INTEGER NOT NULL REFERENCES objekt(id),
benutzer_id INTEGER REFERENCES benutzer(id),
gruppe TEXT
);
-- 5. Kontrolle
CREATE TYPE kontroll_status AS ENUM ('nicht_gestartet', 'in_bearbeitung', 'abgeschlossen', 'abgebrochen');
CREATE TABLE kontrolle (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
erzeugt_von_server_id INTEGER NOT NULL REFERENCES systemknoten(id),
objekt_id INTEGER NOT NULL REFERENCES objekt(id),
benutzer_id INTEGER NOT NULL REFERENCES benutzer(id),
status kontroll_status NOT NULL DEFAULT 'in_bearbeitung',
gestartet_am TIMESTAMPTZ NOT NULL DEFAULT now(),
beendet_am TIMESTAMPTZ,
abbruch_grund TEXT,
signatur BYTEA
);
CREATE TABLE kontrollposition (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
kontrolle_id UUID NOT NULL REFERENCES kontrolle(id),
material_id INTEGER NOT NULL REFERENCES material(id),
sollmenge_snapshot NUMERIC NOT NULL,
istmenge_erfasst NUMERIC NOT NULL,
abweichung BOOLEAN NOT NULL
);
-- 6. Fehlbestand, Nachfüllung, Mindermenge
CREATE TYPE fehlbestand_status AS ENUM ('offen', 'in_bearbeitung', 'nachgefuellt_teilweise', 'erledigt');
CREATE TABLE fehlbestand (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
erzeugt_von_server_id INTEGER NOT NULL REFERENCES systemknoten(id),
objekt_id INTEGER NOT NULL REFERENCES objekt(id),
material_id INTEGER NOT NULL REFERENCES material(id),
standort_id INTEGER NOT NULL REFERENCES standort(id),
sollmenge NUMERIC NOT NULL,
istmenge NUMERIC NOT NULL,
fehlmenge NUMERIC NOT NULL,
entstanden_am TIMESTAMPTZ NOT NULL DEFAULT now(),
festgestellt_von INTEGER NOT NULL REFERENCES benutzer(id),
kontrolle_id UUID REFERENCES kontrolle(id),
ursache TEXT,
verantwortlicher_id INTEGER REFERENCES benutzer(id),
status fehlbestand_status NOT NULL DEFAULT 'offen',
erledigt_am TIMESTAMPTZ
);
CREATE TABLE nachfuellung (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
fehlbestand_id UUID REFERENCES fehlbestand(id),
objekt_id INTEGER NOT NULL REFERENCES objekt(id),
material_id INTEGER NOT NULL REFERENCES material(id),
menge NUMERIC NOT NULL,
benutzer_id INTEGER NOT NULL REFERENCES benutzer(id),
zeitpunkt TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TYPE mindermenge_status AS ENUM ('aktiv', 'abgelaufen', 'beendet_durch_erledigung');
CREATE TABLE mindermengen_genehmigung (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
fehlbestand_id UUID NOT NULL REFERENCES fehlbestand(id),
genehmigt_von INTEGER NOT NULL REFERENCES benutzer(id),
begruendung TEXT NOT NULL,
genehmigt_am TIMESTAMPTZ NOT NULL DEFAULT now(),
ausloesende_kontrolle_id UUID NOT NULL REFERENCES kontrolle(id),
status mindermenge_status NOT NULL DEFAULT 'aktiv',
beendet_am TIMESTAMPTZ,
beendende_kontrolle_id UUID REFERENCES kontrolle(id)
);
-- 7. Historie/Audit
CREATE TABLE historie (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
erzeugt_von_server_id INTEGER NOT NULL REFERENCES systemknoten(id),
zeitpunkt TIMESTAMPTZ NOT NULL DEFAULT now(),
benutzer_id INTEGER REFERENCES benutzer(id),
ereignistyp TEXT NOT NULL,
entitaet_typ TEXT NOT NULL,
entitaet_id TEXT NOT NULL,
alter_wert JSONB,
neuer_wert JSONB,
begruendung TEXT
);
-- 8. Indizes
CREATE INDEX idx_fehlbestand_status ON fehlbestand(status);
CREATE INDEX idx_fehlbestand_objekt ON fehlbestand(objekt_id);
CREATE INDEX idx_fehlbestand_entstanden ON fehlbestand(entstanden_am);
CREATE INDEX idx_objektposition_objekt ON objektposition(objekt_id);
CREATE INDEX idx_historie_entitaet ON historie(entitaet_typ, entitaet_id);
CREATE INDEX idx_zustaendigkeit_benutzer ON zustaendigkeit(benutzer_id);
"""
DOWNGRADE_SQL = """
DROP TABLE IF EXISTS historie;
DROP TABLE IF EXISTS mindermengen_genehmigung;
DROP TABLE IF EXISTS nachfuellung;
DROP TABLE IF EXISTS fehlbestand;
DROP TABLE IF EXISTS kontrollposition;
DROP TABLE IF EXISTS kontrolle;
DROP TABLE IF EXISTS kontrollverantwortung;
DROP TABLE IF EXISTS zustaendigkeit;
DROP TABLE IF EXISTS benutzer_rolle;
DROP TABLE IF EXISTS benutzer;
DROP TABLE IF EXISTS objektposition;
DROP TABLE IF EXISTS objekt;
DROP TABLE IF EXISTS systemknoten;
DROP TABLE IF EXISTS vorlagenposition;
DROP TABLE IF EXISTS beladungsvorlage;
DROP TABLE IF EXISTS material;
DROP TABLE IF EXISTS objekttyp;
DROP TABLE IF EXISTS standort;
DROP TABLE IF EXISTS kategorie;
DROP TABLE IF EXISTS bereich;
DROP TYPE IF EXISTS mindermenge_status;
DROP TYPE IF EXISTS fehlbestand_status;
DROP TYPE IF EXISTS kontroll_status;
DROP TYPE IF EXISTS rolle_typ;
DROP TYPE IF EXISTS objektposition_status;
DROP TYPE IF EXISTS objekt_status;
DROP TYPE IF EXISTS knoten_typ;
DROP TYPE IF EXISTS vorlage_status;
DROP TYPE IF EXISTS materialtyp;
"""
def upgrade() -> None:
op.execute(UPGRADE_SQL)
def downgrade() -> None:
op.execute(DOWNGRADE_SQL)