"""BAN YARO — Wurfverwaltung (Züchter: Würfe & Welpen)""" import logging from datetime import date from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import HTMLResponse from pydantic import BaseModel from typing import Optional from database import db from auth import get_current_user, get_current_user_optional router = APIRouter() logger = logging.getLogger(__name__) # ------------------------------------------------------------------ # Dependency: nur verifizierte Züchter + Admins # ------------------------------------------------------------------ def _require_breeder(user=Depends(get_current_user)): if user["rolle"] not in ("breeder", "admin"): raise HTTPException(403, "Nur für verifizierte Züchter.") return user # ------------------------------------------------------------------ # Schemas # ------------------------------------------------------------------ class LitterCreate(BaseModel): vater_name: Optional[str] = None mutter_name: Optional[str] = None vater_id: Optional[int] = None # FK zucht_hunde mutter_id: Optional[int] = None # FK zucht_hunde geburt_datum: Optional[str] = None # YYYY-MM-DD erwartetes_datum: Optional[str] = None # YYYY-MM-DD welpen_gesamt: Optional[int] = None welpen_verfuegbar: Optional[int] = None beschreibung: Optional[str] = None gesundheitstests: Optional[str] = None preis_spanne: Optional[str] = None status: str = "geplant" # geplant|geboren|verfuegbar|abgeschlossen sichtbar: int = 0 sichtbar_bis: Optional[str] = None class LitterUpdate(BaseModel): vater_name: Optional[str] = None mutter_name: Optional[str] = None vater_id: Optional[int] = None mutter_id: Optional[int] = None geburt_datum: Optional[str] = None erwartetes_datum: Optional[str] = None welpen_gesamt: Optional[int] = None welpen_verfuegbar: Optional[int] = None beschreibung: Optional[str] = None gesundheitstests: Optional[str] = None preis_spanne: Optional[str] = None status: Optional[str] = None sichtbar: Optional[int] = None sichtbar_bis: Optional[str] = None class PuppyCreate(BaseModel): name: Optional[str] = None geschlecht: Optional[str] = None # maennlich|weiblich farbe: Optional[str] = None chip_nr: Optional[str] = None geburtsgewicht: Optional[float] = None # Gramm status: str = "verfuegbar" # verfuegbar|reserviert|abgegeben status_sichtbar: int = 1 notiz: Optional[str] = None class PuppyUpdate(BaseModel): name: Optional[str] = None geschlecht: Optional[str] = None farbe: Optional[str] = None chip_nr: Optional[str] = None geburtsgewicht: Optional[float] = None status: Optional[str] = None status_sichtbar: Optional[int] = None notiz: Optional[str] = None class WeightEntry(BaseModel): gewicht_g: float gemessen_am: str # YYYY-MM-DD # ------------------------------------------------------------------ # Hilfsfunktion: Züchter-Profil des Users ermitteln # ------------------------------------------------------------------ def _get_breeder_profile(user_id: int, conn): row = conn.execute( "SELECT id FROM breeder_profiles WHERE user_id=?", (user_id,) ).fetchone() return row def _check_litter_owner(litter_id: int, user, conn): """Gibt den Wurf zurück wenn der User Eigentümer oder Admin ist.""" litter = conn.execute( "SELECT l.*, bp.user_id AS owner_user_id " "FROM litters l JOIN breeder_profiles bp ON bp.id = l.breeder_id " "WHERE l.id=?", (litter_id,) ).fetchone() if not litter: raise HTTPException(404, "Wurf nicht gefunden.") if user["rolle"] != "admin" and litter["owner_user_id"] != user["id"]: raise HTTPException(403, "Kein Zugriff.") return litter # ------------------------------------------------------------------ # GET /api/litters — öffentliche Übersicht # ------------------------------------------------------------------ @router.get("/litters") async def list_public_litters( rasse: Optional[str] = None, status: Optional[str] = None, ): today = date.today().isoformat() with db() as conn: q = """ SELECT l.*, bp.zwingername, bp.rasse_text, bp.stadt, bp.user_id AS breeder_user_id, u.name AS zuechter_name FROM litters l JOIN breeder_profiles bp ON bp.id = l.breeder_id JOIN users u ON u.id = bp.user_id WHERE l.sichtbar = 1 AND (l.sichtbar_bis IS NULL OR l.sichtbar_bis >= ?) """ params = [today] if status: q += " AND l.status = ?" params.append(status) else: q += " AND l.status IN ('geplant', 'geboren', 'verfuegbar')" if rasse: q += " AND LOWER(bp.rasse_text) LIKE LOWER(?)" params.append(f"%{rasse}%") q += " ORDER BY l.created_at DESC" rows = conn.execute(q, params).fetchall() return [dict(r) for r in rows] # ------------------------------------------------------------------ # GET /api/litters/my — eigene Würfe (Züchter) # ------------------------------------------------------------------ @router.get("/litters/my") async def my_litters(user=Depends(_require_breeder)): with db() as conn: if user["rolle"] == "admin": # Admin ohne eigenes Profil sieht alle Würfe aller Züchter profile = _get_breeder_profile(user["id"], conn) if not profile: rows = conn.execute( "SELECT l.*, bp.zwingername FROM litters l " "JOIN breeder_profiles bp ON bp.id = l.breeder_id " "ORDER BY l.created_at DESC" ).fetchall() return [dict(r) for r in rows] else: profile = _get_breeder_profile(user["id"], conn) if not profile: raise HTTPException(404, "Kein Züchter-Profil vorhanden.") rows = conn.execute( "SELECT * FROM litters WHERE breeder_id=? ORDER BY created_at DESC", (profile["id"],) ).fetchall() return [dict(r) for r in rows] # ------------------------------------------------------------------ # POST /api/litters — neuen Wurf anlegen # ------------------------------------------------------------------ @router.post("/litters", status_code=201) async def create_litter(body: LitterCreate, user=Depends(_require_breeder)): with db() as conn: profile = _get_breeder_profile(user["id"], conn) if not profile: raise HTTPException(404, "Züchter-Profil nicht gefunden.") cur = conn.execute( """INSERT INTO litters (breeder_id, vater_name, mutter_name, vater_id, mutter_id, geburt_datum, erwartetes_datum, welpen_gesamt, welpen_verfuegbar, beschreibung, gesundheitstests, preis_spanne, status, sichtbar, sichtbar_bis) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", ( profile["id"], body.vater_name, body.mutter_name, body.vater_id, body.mutter_id, body.geburt_datum, body.erwartetes_datum, body.welpen_gesamt, body.welpen_verfuegbar, body.beschreibung, body.gesundheitstests, body.preis_spanne, body.status, body.sichtbar, body.sichtbar_bis, ) ) litter_id = cur.lastrowid row = conn.execute( "SELECT * FROM litters WHERE id=?", (litter_id,) ).fetchone() # Tierschutz-Check from welfare_check import check_welfare welfare = check_welfare( conn, profile["id"], vater_id=body.vater_id, mutter_id=body.mutter_id, ) # Welfare-Level speichern conn.execute( "UPDATE litters SET welfare_level=? WHERE id=?", (welfare["level"], litter_id) ) result = dict(row) result["welfare"] = welfare return result # ------------------------------------------------------------------ # POST /api/litters/{id}/welfare-confirm — Tierschutz-Hinweis bestätigt # ------------------------------------------------------------------ @router.post("/litters/{litter_id}/welfare-confirm") async def welfare_confirm(litter_id: int, user=Depends(_require_breeder)): from mailer import send_email import os, logging as _log _logger = _log.getLogger(__name__) with db() as conn: litter = _check_litter_owner(litter_id, user, conn) conn.execute( "UPDATE litters SET welfare_acknowledged=1 WHERE id=?", (litter_id,) ) welfare_level = litter.get("welfare_level", "") if welfare_level == "critical": # Admin benachrichtigen profile = conn.execute( "SELECT bp.zwingername, u.name, u.email " "FROM breeder_profiles bp JOIN users u ON u.id=bp.user_id " "WHERE bp.user_id=?", (user["id"],) ).fetchone() admin_email = os.getenv("ADMIN_EMAIL", "mail@motocamp.de") app_url = os.getenv("APP_URL", "https://banyaro.app") zuechter = profile["name"] if profile else user.get("name", "Unbekannt") zwinger = profile["zwingername"] if profile else "—" eltern = conn.execute( "SELECT vater_name, mutter_name FROM litters WHERE id=?", (litter_id,) ).fetchone() html = f"""

Tierschutz-Hinweis bestätigt

Züchter {zuechter} (Zwinger: {zwinger}) hat einen Wurf mit kritischen Tierschutz-Hinweisen trotzdem angelegt.

Vater: {eltern['vater_name'] or '—'}  ·  Mutter: {eltern['mutter_name'] or '—'}

Wurf-ID: {litter_id}

Im Admin-Bereich prüfen

""" try: await send_email( admin_email, f"[Banyaro Tierschutz] Kritischer Hinweis bestätigt — {zwinger}", html, f"Züchter {zuechter} hat Wurf #{litter_id} trotz kritischer Tierschutz-Hinweise angelegt.", ) except Exception as e: _logger.warning(f"Tierschutz-Admin-Mail fehlgeschlagen: {e}") return {"message": "Bestätigt."} # ------------------------------------------------------------------ # GET /api/litters/{id} — Wurf-Detail (öffentlich wenn sichtbar=1) # ------------------------------------------------------------------ @router.get("/litters/{litter_id}") async def get_litter(litter_id: int, user=Depends(get_current_user_optional)): today = date.today().isoformat() with db() as conn: row = conn.execute( """SELECT l.*, bp.zwingername, bp.rasse_text, bp.stadt, bp.user_id AS owner_user_id, u.name AS zuechter_name FROM litters l JOIN breeder_profiles bp ON bp.id = l.breeder_id JOIN users u ON u.id = bp.user_id WHERE l.id=?""", (litter_id,) ).fetchone() if not row: raise HTTPException(404, "Wurf nicht gefunden.") is_owner = user and ( user["rolle"] == "admin" or row["owner_user_id"] == user["id"] ) # Nicht-öffentliche Würfe nur für Züchter/Admin if not row["sichtbar"] and not is_owner: raise HTTPException(404, "Wurf nicht gefunden.") # Abgelaufene Würfe if row["sichtbar_bis"] and row["sichtbar_bis"] < today and not is_owner: raise HTTPException(404, "Wurf nicht mehr verfügbar.") return dict(row) # ------------------------------------------------------------------ # PUT /api/litters/{id} — Wurf bearbeiten # ------------------------------------------------------------------ @router.put("/litters/{litter_id}") async def update_litter(litter_id: int, body: LitterUpdate, user=Depends(_require_breeder)): with db() as conn: _check_litter_owner(litter_id, user, conn) fields, params = [], [] for field, value in body.model_dump(exclude_none=True).items(): fields.append(f"{field}=?") params.append(value) if not fields: raise HTTPException(400, "Keine Felder zum Aktualisieren.") params.append(litter_id) conn.execute( f"UPDATE litters SET {', '.join(fields)} WHERE id=?", params ) row = conn.execute("SELECT * FROM litters WHERE id=?", (litter_id,)).fetchone() return dict(row) # ------------------------------------------------------------------ # DELETE /api/litters/{id} — Wurf löschen # ------------------------------------------------------------------ @router.delete("/litters/{litter_id}", status_code=204) async def delete_litter(litter_id: int, user=Depends(_require_breeder)): with db() as conn: _check_litter_owner(litter_id, user, conn) conn.execute("DELETE FROM puppy_weights WHERE welpe_id IN (SELECT id FROM puppies WHERE wurf_id=?)", (litter_id,)) conn.execute("DELETE FROM puppies WHERE wurf_id=?", (litter_id,)) conn.execute("DELETE FROM litters WHERE id=?", (litter_id,)) return None # ------------------------------------------------------------------ # GET /api/litters/{id}/puppies — Welpen eines Wurfs # ------------------------------------------------------------------ @router.get("/litters/{litter_id}/puppies") async def list_puppies(litter_id: int, user=Depends(get_current_user_optional)): with db() as conn: litter = conn.execute( """SELECT l.sichtbar, l.sichtbar_bis, bp.user_id AS owner_user_id FROM litters l JOIN breeder_profiles bp ON bp.id = l.breeder_id WHERE l.id=?""", (litter_id,) ).fetchone() if not litter: raise HTTPException(404, "Wurf nicht gefunden.") is_owner = user and ( user["rolle"] == "admin" or litter["owner_user_id"] == user["id"] ) q = "SELECT * FROM puppies WHERE wurf_id=?" params = [litter_id] if not is_owner: q += " AND status_sichtbar=1" rows = conn.execute(q + " ORDER BY created_at ASC", params).fetchall() return [dict(r) for r in rows] # ------------------------------------------------------------------ # POST /api/litters/{id}/puppies — Welpe anlegen # ------------------------------------------------------------------ @router.post("/litters/{litter_id}/puppies", status_code=201) async def add_puppy(litter_id: int, body: PuppyCreate, user=Depends(_require_breeder)): with db() as conn: _check_litter_owner(litter_id, user, conn) cur = conn.execute( """INSERT INTO puppies (wurf_id, name, geschlecht, farbe, chip_nr, geburtsgewicht, status, status_sichtbar, notiz) VALUES (?,?,?,?,?,?,?,?,?)""", ( litter_id, body.name, body.geschlecht, body.farbe, body.chip_nr, body.geburtsgewicht, body.status, body.status_sichtbar, body.notiz, ) ) row = conn.execute( "SELECT * FROM puppies WHERE id=?", (cur.lastrowid,) ).fetchone() return dict(row) # ------------------------------------------------------------------ # PUT /api/litters/puppies/{id} — Welpe bearbeiten # ------------------------------------------------------------------ @router.put("/litters/puppies/{puppy_id}") async def update_puppy(puppy_id: int, body: PuppyUpdate, user=Depends(_require_breeder)): with db() as conn: puppy = conn.execute( """SELECT p.*, l.id AS litter_id, bp.user_id AS owner_user_id FROM puppies p JOIN litters l ON l.id = p.wurf_id JOIN breeder_profiles bp ON bp.id = l.breeder_id WHERE p.id=?""", (puppy_id,) ).fetchone() if not puppy: raise HTTPException(404, "Welpe nicht gefunden.") if user["rolle"] != "admin" and puppy["owner_user_id"] != user["id"]: raise HTTPException(403, "Kein Zugriff.") fields, params = [], [] for field, value in body.model_dump(exclude_none=True).items(): fields.append(f"{field}=?") params.append(value) if not fields: raise HTTPException(400, "Keine Felder zum Aktualisieren.") params.append(puppy_id) conn.execute( f"UPDATE puppies SET {', '.join(fields)} WHERE id=?", params ) row = conn.execute("SELECT * FROM puppies WHERE id=?", (puppy_id,)).fetchone() return dict(row) # ------------------------------------------------------------------ # GET /api/litters/puppies/{id}/weights — Gewichtsverlauf laden # ------------------------------------------------------------------ @router.get("/litters/puppies/{puppy_id}/weights") async def get_weights(puppy_id: int, user=Depends(get_current_user_optional)): with db() as conn: rows = conn.execute( "SELECT id, gewicht_g, gemessen_am FROM puppy_weights WHERE welpe_id=? ORDER BY gemessen_am DESC", (puppy_id,) ).fetchall() return [dict(r) for r in rows] # ------------------------------------------------------------------ # POST /api/litters/puppies/{id}/weight — Gewicht erfassen # ------------------------------------------------------------------ @router.post("/litters/puppies/{puppy_id}/weight", status_code=201) async def add_weight(puppy_id: int, body: WeightEntry, user=Depends(_require_breeder)): with db() as conn: puppy = conn.execute( """SELECT p.id, bp.user_id AS owner_user_id FROM puppies p JOIN litters l ON l.id = p.wurf_id JOIN breeder_profiles bp ON bp.id = l.breeder_id WHERE p.id=?""", (puppy_id,) ).fetchone() if not puppy: raise HTTPException(404, "Welpe nicht gefunden.") if user["rolle"] != "admin" and puppy["owner_user_id"] != user["id"]: raise HTTPException(403, "Kein Zugriff.") cur = conn.execute( "INSERT INTO puppy_weights (welpe_id, gewicht_g, gemessen_am) VALUES (?,?,?)", (puppy_id, body.gewicht_g, body.gemessen_am) ) row = conn.execute( "SELECT * FROM puppy_weights WHERE id=?", (cur.lastrowid,) ).fetchone() return dict(row) # ------------------------------------------------------------------ # GET /api/litters/puppies/{id}/contract — Kaufvertrag als HTML # ------------------------------------------------------------------ @router.get("/litters/puppies/{puppy_id}/contract") async def generate_contract( puppy_id: int, kaeufer_name: str, kaeufer_adresse: str, kaeufer_email: str = "", preis: str = "", user=Depends(_require_breeder), ): with db() as conn: puppy = conn.execute( """SELECT p.*, l.geburt_datum, l.id AS litter_id, bp.user_id AS owner_user_id, bp.zwingername, bp.rasse_text, bp.stadt, u.name AS zuechter_name, u.email AS zuechter_email FROM puppies p JOIN litters l ON l.id = p.wurf_id JOIN breeder_profiles bp ON bp.id = l.breeder_id JOIN users u ON u.id = bp.user_id WHERE p.id=?""", (puppy_id,) ).fetchone() if not puppy: raise HTTPException(404, "Welpe nicht gefunden.") if user["rolle"] != "admin" and puppy["owner_user_id"] != user["id"]: raise HTTPException(403, "Kein Zugriff.") def esc(s): if not s: return "" return (str(s) .replace("&", "&") .replace("<", "<") .replace(">", ">") .replace('"', """)) heute = date.today().strftime("%d.%m.%Y") geschlecht_label = ( "Rüde" if puppy["geschlecht"] == "maennlich" else "Hündin" if puppy["geschlecht"] == "weiblich" else "—" ) geburtsdatum = "" if puppy["geburt_datum"]: try: from datetime import date as _date gd = _date.fromisoformat(puppy["geburt_datum"]) geburtsdatum = gd.strftime("%d.%m.%Y") except Exception: geburtsdatum = esc(puppy["geburt_datum"]) html = f""" Kaufvertrag — {esc(puppy['name'] or 'Welpe')}

Datum: {heute}

Kaufvertrag über einen Welpen

Rassehund · {esc(puppy['rasse_text'] or '')}

Verkäufer (Züchter)

Zwingername{esc(puppy['zwingername'] or '—')}
Name{esc(puppy['zuechter_name'] or '—')}
Ort{esc(puppy['stadt'] or '—')}
E-Mail{esc(puppy['zuechter_email'] or '—')}

Käufer

Name{esc(kaeufer_name)}
Adresse{esc(kaeufer_adresse)}
E-Mail{esc(kaeufer_email) if kaeufer_email else '—'}

Welpe

Name{esc(puppy['name'] or '—')}
Geschlecht{geschlecht_label}
Rasse{esc(puppy['rasse_text'] or '—')}
Geburtsdatum{geburtsdatum or '—'}
Chip-Nr.{esc(puppy['chip_nr'] or '—')}
Farbe / Fell{esc(puppy['farbe'] or '—')}

Kaufpreis

Vereinbarter Preis{esc(preis) if preis else '—'}

Allgemeine Vereinbarungen

Der Käufer bestätigt, den Welpen in einem einwandfreien Gesundheitszustand entgegengenommen zu haben. Der Verkäufer sichert zu, dass der Welpe nach bestem Wissen und Gewissen aufgezogen wurde und die angegebenen Gesundheitsinformationen der Wahrheit entsprechen. Der Käufer verpflichtet sich, den Welpen artgerecht zu halten und tierärztlich versorgen zu lassen.


Ort, Datum & Unterschrift Verkäufer


Ort, Datum & Unterschrift Käufer

""" return HTMLResponse(content=html)