Changes to the Buchungs Processing for the AGB Accepting and the AVV also the the email and workflow was changed

This commit is contained in:
2026-10-03 00:09:44 +02:00
parent fa6fa56fd4
commit 00b92084a0
7 changed files with 205 additions and 14 deletions
+115 -4
View File
@@ -83,6 +83,8 @@ BOOKING_PRICE_MAP = {
"testversion": 0.0,
}
BOOKING_ADMIN_EMAIL_FALLBACK = os.environ.get("BOOKING_ADMIN_EMAIL", "").strip()
AGB_FILENAME = "AGB Invario.pdf"
AVV_FILENAME = "AVV Invario.pdf"
@app.after_request
@@ -97,6 +99,18 @@ def set_security_headers(response):
return response
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
def _get_avv_version() -> str:
path = os.path.join(BASE_DIR, "static", "downloads", AVV_FILENAME)
try:
with open(path, "rb") as handle:
digest = hashlib.sha256(handle.read()).hexdigest()
return f"{AVV_FILENAME} | SHA-256: {digest}"
except OSError:
return AVV_FILENAME
INVOICE_UPLOAD_DIR = os.path.join(BASE_DIR, "static", "uploads", "invoices")
TUTORIAL_UPLOAD_DIR = os.path.join(BASE_DIR, "static", "uploads", "tutorials")
LEGAL_DOWNLOAD_DIR = os.path.join(BASE_DIR, "static", "downloads", "legal")
@@ -276,6 +290,11 @@ def _build_legal_pdf_bundle() -> BytesIO:
with zipfile.ZipFile(bundle, mode="w", compression=zipfile.ZIP_DEFLATED) as archive:
for filename, _, _ in LEGAL_DOWNLOAD_FILES:
archive.write(os.path.join(LEGAL_DOWNLOAD_DIR, filename), arcname=filename)
static_download_dir = os.path.join(BASE_DIR, "static", "downloads")
for filename in (AGB_FILENAME, AVV_FILENAME):
source = os.path.join(static_download_dir, filename)
if os.path.isfile(source):
archive.write(source, arcname=filename)
bundle.seek(0)
return bundle
@@ -1940,11 +1959,21 @@ def register():
password = request.form.get("password") or ""
password_repeat = request.form.get("password_repeat") or ""
marketing_opt_in = True if request.form.get("marketing_opt_in") == "on" else False
legal_consents = {
"agb": request.form.get("accept_agb") == "on",
"privacy": request.form.get("accept_privacy") == "on",
"contract": request.form.get("accept_contract") == "on",
"avv": request.form.get("accept_avv") == "on",
}
if not username or not school_name or not contact_person or not email or not password or not password_repeat:
flash("Bitte alle Felder ausfüllen.", "error")
return redirect(url_for("register"))
if not all(legal_consents.values()):
flash("Bitte bestätigen Sie AGB, Datenschutzerklärung, Vertragsbedingungen und AVV einzeln.", "error")
return redirect(url_for("register"))
if not _validate_username(username):
flash("Benutzername muss 3-30 Zeichen lang sein und nur Buchstaben, Zahlen, - und _ enthalten.", "error")
return redirect(url_for("register"))
@@ -1982,7 +2011,9 @@ def register():
username, password, school_name, contact_person, email,
marketing_opt_in=marketing_opt_in,
verified=False,
verification_token=token
verification_token=token,
legal_consents=legal_consents,
legal_consents_accepted_at=_utc_now_iso(),
):
flash("Benutzer konnte nicht erstellt werden.", "error")
return redirect(url_for("register"))
@@ -2344,6 +2375,7 @@ def background_booking_provisioning(
# 7. Bestätigungs-E-Mail versenden
try:
if user_email:
_ensure_legal_pdf_files()
formatted_date = datetime.today().strftime('%d.%m.%Y')
send_accreditation_email(
@@ -2727,6 +2759,39 @@ def _get_user_trial_instance(username: str) -> dict | None:
"is_active": bool(instance.get("is_trial") and instance.get("trial_state") in {"active", "in_progress", "expired"}),
}
def _get_booking_form_defaults(username: str) -> dict:
defaults = {}
if not username:
return defaults
try:
user = user_store.get_user(username) or {}
defaults.update({
"school_name": user.get("school_name") or "",
"contact_person": user.get("contact_person") or user.get("name") or "",
"contact_email": user.get("email") or user.get("Email") or "",
})
except Exception:
pass
client, collection = _get_collection("instance_requests")
try:
request_doc = collection.find_one(
{"username": username, "booking_data": {"$exists": True}},
sort=[("created_at", -1)],
)
finally:
client.close()
if request_doc and isinstance(request_doc.get("booking_data"), dict):
defaults.update(request_doc["booking_data"])
return {
key: _sanitize_text(value, 600)
for key, value in defaults.items()
if value not in (None, "") and key not in {"agreements_accepted", "confirm_subdomain_change"}
}
@app.route('/booking/payment', methods=['GET', 'POST'])
@login_required
def booking_payment():
@@ -2799,8 +2864,30 @@ def booking_payment():
"confirm_subdomain_change": (request.values.get("confirm_subdomain_change") or "").strip().lower() in {"1", "on", "true", "yes"},
"notes": _sanitize_text(request.values.get("notes") or "", 600),
"agreements_accepted": (request.values.get("agreements_accepted") or "").strip().lower() in {"1", "on", "true", "yes"},
"accept_agb": (request.values.get("accept_agb") or "").strip().lower() in {"1", "on", "true", "yes"},
"accept_privacy": (request.values.get("accept_privacy") or "").strip().lower() in {"1", "on", "true", "yes"},
"accept_contract": (request.values.get("accept_contract") or "").strip().lower() in {"1", "on", "true", "yes"},
"accept_avv": (request.values.get("accept_avv") or "").strip().lower() in {"1", "on", "true", "yes"},
"avv_version": _get_avv_version(),
}
booking_defaults = _get_booking_form_defaults(session.get("username") or "")
for field in (
"school_name",
"contact_person",
"contact_email",
"contact_phone",
"school_number",
"billing_organization",
"billing_street",
"billing_zip",
"billing_city",
"billing_reference",
"notes",
):
if not form_data[field] and booking_defaults.get(field):
form_data[field] = booking_defaults[field]
if not form_data["tenant_slug"] and trial_instance:
form_data["tenant_slug"] = trial_instance.get("subdomain") or ""
@@ -2851,8 +2938,8 @@ def booking_payment():
flash(err_msg, "error")
return render_template("schnell_buchung.html", **review_data)
if not form_data["agreements_accepted"]:
err_msg = "Bitte Nutzungsbedingungen, Lizenzvereinbarung und Datenschutzerklärung bestätigen."
if not all(form_data[field] for field in ("accept_agb", "accept_privacy", "accept_contract", "accept_avv")):
err_msg = "Bitte bestätigen Sie AGB, Datenschutzerklärung, Vertragsbedingungen und die konkrete AVV-Fassung einzeln."
if is_json_request:
return jsonify({"success": False, "error": err_msg}), 400
flash(err_msg, "error")
@@ -2865,6 +2952,10 @@ def booking_payment():
flash(err_msg, "error")
return render_template("schnell_buchung.html", **review_data)
form_data["agreements_accepted"] = True
form_data["legal_consents_accepted_at"] = _utc_now_iso()
form_data["legal_consent_username"] = session.get("username") or ""
assigned_admin = _select_booking_admin()
assigned_admin_username = assigned_admin.get("username") if assigned_admin else ""
assigned_admin_display_name = assigned_admin.get("display_name") if assigned_admin else ""
@@ -3878,7 +3969,7 @@ def vertrag():
@app.route('/avv')
def avv():
return render_template("avv.html")
return render_template("avv.html", avv_version=_get_avv_version())
@app.route('/instance_request/check_subdomain', methods=['GET'])
def check_subdomain():
@@ -3940,6 +4031,26 @@ def download_legal_pdfs_bundle():
download_name="invario-rechtliche-dokumente.zip",
)
@app.route('/downloads/agb-invario.pdf')
def download_agb_pdf():
return send_from_directory(
os.path.join(BASE_DIR, "static", "downloads"),
AGB_FILENAME,
as_attachment=True,
download_name=AGB_FILENAME,
)
@app.route('/downloads/avv-invario.pdf')
def download_avv_pdf():
return send_from_directory(
os.path.join(BASE_DIR, "static", "downloads"),
AVV_FILENAME,
as_attachment=True,
download_name=AVV_FILENAME,
)
@app.route('/logout')
def logout():
session.pop('username', None)
+31
View File
@@ -0,0 +1,31 @@
{% extends "base.html" %}
{% block title %}{{ avv_title|default('Auftragsverarbeitungsvertrag') }}{% endblock %}
{% block content %}
<section class="panel legal-document">
<h1>Auftragsverarbeitungsvertrag</h1>
<p><strong>Verbindliche Fassung: {{ avv_version }}</strong></p>
<p>Diese Fassung wird als Anlage zum Nutzungsvertrag zwischen der Invario UG und der jeweiligen Schule vereinbart.</p>
<h2>1. Gegenstand und Dauer</h2>
<p>Der Auftragsverarbeiter verarbeitet personenbezogene Daten im Auftrag der Schule zur Bereitstellung der Invario-Software. Die Verarbeitung beginnt mit der Bereitstellung und endet mit Beendigung des Nutzungsvertrags.</p>
<h2>2. Art und Zweck der Verarbeitung</h2>
<p>Die Verarbeitung umfasst insbesondere Speicherung, Abruf, Organisation und Verwaltung von Schul-, Nutzer-, Termin- und Inventardaten. Eine Verarbeitung zu eigenen Zwecken des Auftragsverarbeiters findet nicht statt.</p>
<h2>3. Weisungen und Vertraulichkeit</h2>
<p>Der Auftragsverarbeiter verarbeitet Daten nur auf dokumentierte Weisung der Schule und verpflichtet eingesetztes Personal zur Vertraulichkeit.</p>
<h2>4. Technische und organisatorische Maßnahmen</h2>
<p>Der Auftragsverarbeiter schützt die Daten durch Zugriffskontrollen, Verschlüsselung der Transportwege, Datensicherungen und Protokollierung. Die konkreten Maßnahmen werden regelmäßig auf ihre Wirksamkeit überprüft.</p>
<h2>5. Unterauftragsverarbeiter und Löschung</h2>
<p>Unterauftragsverarbeiter werden nur im Rahmen der gesetzlichen Anforderungen eingesetzt. Nach Vertragsende werden die Daten nach Weisung der Schule gelöscht oder herausgegeben.</p>
<h2>6. Version und Änderungen</h2>
<p>Maßgeblich ist ausschließlich die oben bezeichnete PDF-Fassung. Änderungen werden als neue, eindeutig erkennbare Datei mit neuer Prüfsumme bereitgestellt und nicht stillschweigend durch Austausch dieses Dokuments vorgenommen.</p>
<p><a href="{{ url_for('download_avv_pdf') }}">Diese konkrete AVV-Fassung als PDF herunterladen</a></p>
</section>
{% endblock %}
+1
View File
@@ -31,6 +31,7 @@
<h2>2. Datenschutzbeauftragter</h2>
<p>Unseren Datenschutzbeauftragten erreichen Sie unter:</p>
<p>
<strong>Simon V. Brieger</strong><br>
info@invario-software.de
</p>
</section>
+2 -2
View File
@@ -140,14 +140,14 @@
<h2>12. Geltendes Recht</h2>
<p>
Für alle Rechtsbeziehungen zwischen dem Nutzer und uns gilt ausschließlich deutsches Recht.
Es gilt das TMG und das MDStV in ihrer jeweils aktuellen Fassung. Erfüllungsort ist Musterstadt.
Es gilt das TMG und das MDStV in ihrer jeweils aktuellen Fassung. Erfüllungsort ist Bruckmühl.
</p>
</section>
<section>
<h2>13. Kontakt zum Datenschutzbeauftragten</h2>
<p>
<strong>Datenschutzbeauftragter:</strong><br>
<strong>Datenschutzbeauftragter: Simon V. Brieger</strong><br>
E-Mail: support@invario-software.de<br>
Telefon: 08061 / 3497799<br>
</p>
+38
View File
@@ -69,6 +69,24 @@
cursor: pointer;
}
.legal-consents {
margin: 1.2rem 0 1.3rem;
padding: 1rem;
border: 1px solid #d4e0e9;
border-radius: 12px;
background: #f7fafc;
}
.legal-consents h2 {
margin: 0 0 0.7rem;
font-size: 1rem;
color: #0f344d;
}
.legal-consents .checkbox-line {
margin: 0.75rem 0;
}
.auth-btn {
width: 100%;
border: none;
@@ -127,6 +145,26 @@
<span>Ja, ich möchte E-Mails zu Produktupdates, Neuigkeiten und Marketinginformationen erhalten. (Abmeldung jederzeit möglich)</span>
</label>
<fieldset class="legal-consents">
<h2>Verbindliche Zustimmung zur Registrierung</h2>
<label class="checkbox-line">
<input id="accept_agb" name="accept_agb" type="checkbox" required>
<span>Ich habe die <a href="{{ url_for('nutzungsbedingungen') }}" target="_blank" rel="noopener noreferrer">Allgemeinen Geschäftsbedingungen (AGB) und Lizenzbedingungen</a> gelesen und akzeptiere sie verbindlich.</span>
</label>
<label class="checkbox-line">
<input id="accept_privacy" name="accept_privacy" type="checkbox" required>
<span>Ich habe die <a href="{{ url_for('datenschutz') }}" target="_blank" rel="noopener noreferrer">Datenschutzerklärung</a> gelesen und akzeptiere die dort beschriebene Verarbeitung meiner personenbezogenen Daten.</span>
</label>
<label class="checkbox-line">
<input id="accept_contract" name="accept_contract" type="checkbox" required>
<span>Ich habe die <a href="{{ url_for('vertrag') }}" target="_blank" rel="noopener noreferrer">Vertragsbedingungen</a> gelesen und akzeptiere sie für die Nutzung der angebotenen Leistungen.</span>
</label>
<label class="checkbox-line">
<input id="accept_avv" name="accept_avv" type="checkbox" required>
<span>Ich habe den <a href="{{ url_for('avv') }}" target="_blank" rel="noopener noreferrer">Auftragsverarbeitungsvertrag (AVV)</a> gelesen und stimme seiner Geltung zu, soweit er auf meine Nutzung Anwendung findet.</span>
</label>
</fieldset>
<button class="auth-btn" type="submit">Konto erstellen</button>
</form>
<p class="auth-meta">Bereits registriert? <a href="{{ url_for('login') }}">Zum Login</a></p>
+15 -7
View File
@@ -402,13 +402,21 @@
</div>
<div class="booking-review__field booking-review__field--full booking-review__consent">
<label for="agreements_accepted">
<input id="agreements_accepted" name="agreements_accepted" type="checkbox" required>
<span>
Ich habe die <a href="{{ url_for('nutzungsbedingungen') }}" target="_blank" rel="noopener" style="color: #1a68d0; font-weight: 700;">AGB (PDF)</a>,
die <a href="{{ url_for('vertrag') }}" target="_blank" rel="noopener" style="color: #1a68d0; font-weight: 700;">Vertragsbedingungen</a> sowie den gesetzlich erforderlichen
<a href="{{ url_for('avv') }}" target="_blank" rel="noopener" style="color: #1a68d0; font-weight: 700;">Auftragsverarbeitungsvertrag (AVV)</a> gelesen und stimme diesen Dokumenten verbindlich zu.
</span>
<label for="accept_agb">
<input id="accept_agb" name="accept_agb" type="checkbox" required {% if form_data.accept_agb %}checked{% endif %}>
<span>Ich habe die <a href="{{ url_for('download_agb_pdf') }}" target="_blank" rel="noopener">AGB und Lizenzbedingungen als PDF</a> gelesen und akzeptiere sie verbindlich.</span>
</label>
<label for="accept_privacy">
<input id="accept_privacy" name="accept_privacy" type="checkbox" required {% if form_data.accept_privacy %}checked{% endif %}>
<span>Ich habe die <a href="{{ url_for('datenschutz') }}" target="_blank" rel="noopener">Datenschutzerklärung</a> gelesen und akzeptiere die dort beschriebene Verarbeitung.</span>
</label>
<label for="accept_contract">
<input id="accept_contract" name="accept_contract" type="checkbox" required {% if form_data.accept_contract %}checked{% endif %}>
<span>Ich habe die <a href="{{ url_for('vertrag') }}" target="_blank" rel="noopener">Vertragsbedingungen</a> gelesen und akzeptiere sie verbindlich.</span>
</label>
<label for="accept_avv">
<input id="accept_avv" name="accept_avv" type="checkbox" required {% if form_data.accept_avv %}checked{% endif %}>
<span>Ich schließe den <a href="{{ url_for('download_avv_pdf') }}" target="_blank" rel="noopener">Auftragsverarbeitungsvertrag in der Fassung {{ form_data.avv_version }}</a> als Anlage zum Nutzungsvertrag ab. Ich kann diese Fassung herunterladen.</span>
</label>
</div>
</div>
+3 -1
View File
@@ -42,7 +42,7 @@ def check_nm_pwd(username, password):
client.close()
def add_user(username, password, school_name="", contact_person="", email="", marketing_opt_in=False, verified=False, verification_token=None):
def add_user(username, password, school_name="", contact_person="", email="", marketing_opt_in=False, verified=False, verification_token=None, legal_consents=None, legal_consents_accepted_at=None):
"""
Legt einen neuen Benutzer mit allen Verifizierungsdaten in MongoDB an.
"""
@@ -62,6 +62,8 @@ def add_user(username, password, school_name="", contact_person="", email="", ma
'contact_person': contact_person,
'email': (email or '').strip().lower(),
'marketing_opt_in': marketing_opt_in,
'legal_consents': legal_consents or {},
'legal_consents_accepted_at': legal_consents_accepted_at,
'verified': verified,
'verification_token': hashing(verification_token) if verification_token else None,
}