Feature: QR-Kontingente für Partner — Bestellung, Übergabe, Rückverfolgung
Partner verteilen gedruckte QR-Codes (Sticker/Flyer); jeder physische Code
ist einzeln rückverfolgbar von Scan bis Registrierung.
Backend:
- partner_qr_batches + partner_qr_codes (Token 8-stellig, ohne 0/O/1/l/I),
users.referred_qr, partner_codes.owner_user_id (+Backfill über referred_by)
- /q/{token}: Scan zählen (scans, first/last_scan_at) → Redirect
/?ref=CODE&qr=TOKEN — dockt am bestehenden Referral-Flow an
- Registrierung: qr_token wird nur zugeordnet, wenn er zum eingelösten
Partner-Code gehört (Manipulationsschutz)
- Admin: Kontingent bestellen (max 500), Liste mit Scans/Registrierungen,
Löschen (Zweiklick), druckfertiges A4-PDF (segno+fpdf2, 3×4 Grid mit
Kurz-URL + laufender Nummer), Code-Besitzer zuordnen
- Partner-Self-Service: /partner/my-qr (+PDF) für Code-Besitzer
Frontend:
- Admin-Partner-Tab: Karte 'QR-Kontingente' (Bestellung, Stats, PDF, Besitzer)
- Partner-Profil: 'Meine QR-Codes' mit Scans/Registrierungen + PDF-Download
- boot.js/app.js speichern ?qr=, Registrierung schickt qr_token mit
Neu: segno==1.6.6 (pure-python QR). Tests: 5 neue (PDF, Scan-Zählung,
Attribution, Fremd-Token-Schutz, Self-Service). Suite: 51 passed.
This commit is contained in:
parent
cadfb24a8d
commit
f604ab7c4f
16 changed files with 621 additions and 23 deletions
|
|
@ -623,6 +623,10 @@ def _migrate(conn_factory):
|
|||
("users", "is_partner", "INTEGER NOT NULL DEFAULT 0"),
|
||||
("users", "founder_number", "INTEGER"),
|
||||
("users", "is_founder_pending", "INTEGER NOT NULL DEFAULT 0"),
|
||||
# QR-Rückverfolgung: über welchen physischen QR-Code (Sticker/Flyer) kam die Registrierung
|
||||
("users", "referred_qr", "TEXT"),
|
||||
# Partner-Code → Besitzer (für Self-Service: eigene QR-Kontingente + Stats einsehen)
|
||||
("partner_codes", "owner_user_id", "INTEGER"),
|
||||
# Passwort-Zurücksetzen
|
||||
("users", "password_reset_token", "TEXT"),
|
||||
("users", "password_reset_expires", "TEXT"),
|
||||
|
|
@ -1685,6 +1689,48 @@ def _migrate(conn_factory):
|
|||
except Exception as e:
|
||||
logger.warning(f"Migration partner_profiles: {e}")
|
||||
|
||||
# QR-Kontingente für Partner (gedruckte Sticker/Flyer mit Rückverfolgung)
|
||||
# Jeder physische QR-Code hat einen eigenen Token → Scan- und
|
||||
# Registrierungs-Tracking pro Einzelcode und pro Kontingent.
|
||||
try:
|
||||
conn.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS partner_qr_batches (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
partner_code_id INTEGER NOT NULL REFERENCES partner_codes(id) ON DELETE CASCADE,
|
||||
label TEXT NOT NULL,
|
||||
quantity INTEGER NOT NULL,
|
||||
created_by INTEGER REFERENCES users(id),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS partner_qr_codes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
batch_id INTEGER NOT NULL REFERENCES partner_qr_batches(id) ON DELETE CASCADE,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
seq INTEGER NOT NULL,
|
||||
scans INTEGER NOT NULL DEFAULT 0,
|
||||
first_scan_at TEXT,
|
||||
last_scan_at TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_pqr_token ON partner_qr_codes(token);
|
||||
CREATE INDEX IF NOT EXISTS idx_pqr_batch ON partner_qr_codes(batch_id);
|
||||
""")
|
||||
logger.info("Migration: partner_qr Tabellen bereit.")
|
||||
except Exception as e:
|
||||
logger.warning(f"Migration partner_qr: {e}")
|
||||
try:
|
||||
# Backfill: Partner, die sich mit ihrem eigenen Code registriert haben,
|
||||
# als Code-Besitzer verknüpfen (für Self-Service-Zugriff auf QR-Stats).
|
||||
# Eigener try-Block: owner_user_id kommt auf frischen DBs erst im 2nd pass.
|
||||
conn.execute("""
|
||||
UPDATE partner_codes SET owner_user_id = (
|
||||
SELECT u.id FROM users u
|
||||
WHERE u.referred_by = -partner_codes.id AND u.is_partner = 1
|
||||
LIMIT 1
|
||||
) WHERE owner_user_id IS NULL
|
||||
""")
|
||||
except Exception as e:
|
||||
logger.debug(f"Backfill partner_codes.owner_user_id übersprungen: {e}")
|
||||
|
||||
# Outreach-Log (Admin-E-Mail-Versand)
|
||||
try:
|
||||
conn.executescript("""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue