Züchter-Bereich (Hub) + Settings-Partner-Karte raus + Admin: alle Code-Einlösungen mit Kanal

- Neue Seite #breeder-dashboard (Welten-Chip 'Züchter' role:breeder in HUND,
  ersetzt die Einzel-Chips Zuchtkartei + Wurfverw.; beide FABs wandern an den
  neuen Chip; Läufigkeit bleibt eigener Chip in HUND, Rene-Vorgabe):
  Zwinger-Karte (Name, verifiziert-Badge, Profil-Editor), Wurfverwaltung mit
  Wurf-Anzahl, Zuchtkartei mit Hunde-Anzahl. Einzelseiten bleiben erreichbar.
- Settings: Partner-Karte entfernt — der 🤝-Welten-Chip ist der Einstieg.
- Admin 'Aktive Codes': 👥 zeigt jetzt ALLE Einlösungen eines Codes mit
  Kanal-Badge (QR #seq aus Kontingent vs. Link/manuell), Datum und
  Bestätigt-Status — Endpoint /admin/partner/codes/{id}/registrations.
Suite: 55 passed.
This commit is contained in:
rene 2026-06-07 19:55:51 +02:00
parent 6a6a09d879
commit ed7c469c6a
11 changed files with 241 additions and 44 deletions

View file

@ -125,6 +125,30 @@ def test_registration_with_qr_only(client, admin):
assert row["referred_qr"] == token
def test_code_registrations_with_channel(client, admin):
"""Admin-Liste aller Code-Einloesungen unterscheidet QR-Sticker und Link/manuell."""
code = _create_code(client, admin)
batch = _create_batch(client, admin, code["id"], quantity=1)
token = _batch_tokens(batch["id"])[0]
# 1x via QR, 1x via Code direkt
client.post("/api/auth/register", json={
"email": f"ch1-{secrets.token_hex(4)}@example.com", "password": "QrTest1234!",
"name": f"ch1{secrets.token_hex(3)}", "ref_code": code["code"], "qr_token": token,
})
client.post("/api/auth/register", json={
"email": f"ch2-{secrets.token_hex(4)}@example.com", "password": "QrTest1234!",
"name": f"ch2{secrets.token_hex(3)}", "ref_code": code["code"],
})
r = client.get(f"/api/admin/partner/codes/{code['id']}/registrations", headers=admin["headers"])
assert r.status_code == 200
regs = r.json()
assert len(regs) == 2
channels = {(x["qr_seq"] or 0) for x in regs}
assert channels == {0, 1} # einer ohne QR (None), einer über Sticker #1
def test_paused_code_not_redeemable(client, admin):
"""Pausierter Code (Notbremse) -> keine Einloesung, Info-Endpoint 404; reaktivierbar."""
code = _create_code(client, admin)