diff --git a/Website/main.py b/Website/main.py index ce2c761..3aff9d1 100644 --- a/Website/main.py +++ b/Website/main.py @@ -2206,7 +2206,19 @@ def build_module_config_string(package_list: list) -> str: ] return " ".join(config_parts) -def background_booking_provisioning(app_instance, prov_id, subdomain, package_list, user_id, school_name, user_email, username, address, price, software_name): +def background_booking_provisioning( + app_instance, + prov_id, + subdomain, + package_list, + user_id, + school_name, + user_email, + username, + address, + price, + software_name="Invario Inventarsystem" +): """Hintergrund-Worker, der die Tenant-Bereitstellung über das Host-Watcher-Triggersystem auslöst.""" with app_instance.app_context(): req_client, req_col = _get_collection("instance_requests") @@ -2214,7 +2226,7 @@ def background_booking_provisioning(app_instance, prov_id, subdomain, package_li # 1. Status auf "in_progress" setzen req_col.update_one({"_id": ObjectId(prov_id)}, {"$set": {"provision_status": "in_progress"}}) - # 2. Naechsten Port und Admin-Passwort ermitteln + # 2. Nächsten Port und Admin-Passwort ermitteln inst_client_check, inst_col_check = _get_collection("instances") try: existing_instances = list(inst_col_check.find({}, {"https_port": 1, "port": 1})) @@ -2232,7 +2244,7 @@ def background_booking_provisioning(app_instance, prov_id, subdomain, package_li # Generiere den korrekten Modul-String module_config_str = build_module_config_string(package_list) - # 3. Trigger-Datei fuer den Host-Watcher schreiben + # 3. Trigger-Datei für den Host-Watcher schreiben os.makedirs(TRIGGER_DIR, exist_ok=True) trigger_file = os.path.join(TRIGGER_DIR, f"{subdomain}.json") @@ -2248,13 +2260,13 @@ def background_booking_provisioning(app_instance, prov_id, subdomain, package_li with open(trigger_file, "w") as f: json.dump(trigger_payload, f) - print(f"[INFO] Trigger-Datei fuer Host-Watcher erstellt: {trigger_file} mit Payload: {trigger_payload}") + print(f"[INFO] Trigger-Datei für Host-Watcher erstellt: {trigger_file} mit Payload: {trigger_payload}") # 4. Instanz-Eintrag in der Datenbank registrieren domain = f"{subdomain}.invario-software.de" instance_doc = { "owner_id": user_id, - "owner_username": username, # <--- HIER HINZUGEFÜGT + "owner_username": username, "school_name": school_name, "subdomain": subdomain, "domain": domain, @@ -2272,9 +2284,11 @@ def background_booking_provisioning(app_instance, prov_id, subdomain, package_li finally: inst_client.close() - # 5. Bestaetigungs-E-Mail versenden + # 5. Bestätigungs-E-Mail inkl. Vertrag & Anhängen versenden try: if user_email: + formatted_date = datetime.today().strftime('%d.%m.%Y') + send_accreditation_email( recipient=user_email, domain=domain, @@ -2283,9 +2297,10 @@ def background_booking_provisioning(app_instance, prov_id, subdomain, package_li school_name=school_name, address=address, price=price, - date=_utc_now_iso(), - software_name=software_name, + date=formatted_date, + software_name=software_name ) + except Exception as mail_err: print(f"[WARNING] E-Mail-Versand fehlgeschlagen: {mail_err}") @@ -2302,10 +2317,10 @@ def background_booking_provisioning(app_instance, prov_id, subdomain, package_li "updated_at": _utc_now_iso() }} ) - print(f"[SUCCESS] Provisioning-Trigger erfolgreich gesendet fuer {subdomain}") + print(f"[SUCCESS] Provisioning-Trigger erfolgreich gesendet für {subdomain}") except Exception as e: - print(f"[ERROR] Async Provisioning fehlgeschlagen fuer {subdomain}:") + print(f"[ERROR] Async Provisioning fehlgeschlagen für {subdomain}:") traceback.print_exc() req_col.update_one({"_id": ObjectId(prov_id)}, {"$set": {"provision_status": "failed", "last_error": str(e)}}) finally: @@ -2350,7 +2365,6 @@ def booking_payment(): flow_config = BOOKING_FLOW_MAP[booking_flow] - # Auslesen aller Formularfelder (inkl. tenant_slug und Rechnungsdaten) form_data = { "contact_person": _sanitize_text(request.values.get("contact_person") or "", 120), "school_name": _sanitize_text(request.values.get("school_name") or "", 160), @@ -2426,21 +2440,8 @@ def booking_payment(): assigned_admin_display_name = assigned_admin.get("display_name") if assigned_admin else "" assigned_admin_email = (assigned_admin.get("email") if assigned_admin else "") or BOOKING_ADMIN_EMAIL_FALLBACK - message = ( - f"{flow_config['message_prefix']} {selected_package} {flow_config['message_suffix']}\n" - f"Ansprechpartner: {form_data['contact_person']}\n" - f"Schule: {form_data['school_name']} (Dienstst.-Nr: {form_data['school_number']})\n" - f"E-Mail: {form_data['contact_email']}\n" - f"Telefon: {form_data['contact_phone'] or '-'}\n" - f"Schulträger/Amt: {form_data['billing_organization']}\n" - f"Rechnungsadresse: {form_data['billing_street']}, {form_data['billing_zip']} {form_data['billing_city']}\n" - f"Kassenzeichen: {form_data['billing_reference'] or '-'}\n" - f"Gewünschte Subdomain: {form_data['tenant_slug']}\n" - f"Notizen: {form_data['notes'] or '-'}" - ) - try: - # FLOW 1: Beratung / Gesprächs-Buchung + # FLOW 1: Consultation / Chat if booking_flow == "consultation": client, col = _get_collection("chat_messages") try: @@ -2449,7 +2450,7 @@ def booking_payment(): "username": session.get("username"), "sender": session.get("display_name") or session.get("username"), "sender_role": "user", - "message": message, + "message": f"Gesprächswunsch bezüglich: {selected_package}", "created_at": _utc_now_iso(), "booking_flow": booking_flow, "booking_package": package_raw, @@ -2465,18 +2466,16 @@ def booking_payment(): flash(f"Gesprächstermin für {selected_package} wurde an das Team gesendet.", "success") return redirect(url_for("user_chat")) - # FLOW 2: Kostenpflichtige Direkt-Buchung mit Provisioning via Watcher + # FLOW 2: Direktbuchung mit Provisioning via Host-Watcher if booking_flow == "payment": booking_number = f"BOOK-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}" - # Schul-URL aus dem Formular bevorzugen raw_base = form_data.get("tenant_slug") or form_data.get("school_name") or session.get("username") or booking_number base = _slugify_subdomain(raw_base).lower()[:51] - # Prüfung auf doppelte Subdomains via MongoDB 'instances' Collection - inst_check_client, inst_check_col = _get_collection("instances") + inst_check_client, inst_col_check = _get_collection("instances") try: - existing_docs = list(inst_check_col.find({}, {"subdomain": 1})) + existing_docs = list(inst_col_check.find({}, {"subdomain": 1})) existing_subdomains = [d.get("subdomain") for d in existing_docs if d.get("subdomain")] except Exception as e: print(f"[WARNING] Konnte bestehende Subdomains nicht prüfen: {e}") @@ -2523,29 +2522,37 @@ def booking_payment(): username = session.get("username") user_email = session.get("email") or form_data.get("contact_email") address_process = f"{form_data['billing_street']}, {form_data['billing_zip']} {form_data['billing_city']}" + price_str = f"{total_price:.2f}".replace('.', ',') - # Startet den asynchronen Provisioning-Thread + # Startet den asynchronen Provisioning-Thread mit exakten Parametern thread = threading.Thread( target=background_booking_provisioning, - args=(app_obj, prov_id, subdomain, package_list, user_id, form_data["school_name"], user_email, username, address_process, total_price, selected_package) + args=( + app_obj, + prov_id, + subdomain, + package_list, + user_id, + form_data["school_name"], + user_email, + username, + address_process, + price_str, + selected_package + ) ) thread.start() print(f"[INFO] Background Provisioning Trigger gestartet für Subdomain: {subdomain}") - # ANPASSUNG FÜR BESSERE WARTEZEIT: - # Wir geben Metadaten mit, wie lange das Setup ca. dauern wird. if is_json_request: return jsonify({ "success": True, "slug": subdomain, "message": "Instanz wird eingerichtet...", - "estimated_seconds": 45, # Erwartete Dauer in Sekunden (anpassbar) - "status_poll_interval_ms": 3000 # Wie oft soll das Frontend fragen? (3 Sek) + "estimated_seconds": 45, + "status_poll_interval_ms": 3000 }), 200 - # Falls es KEIN AJAX-Request war: - # Wir hängen den Parameter "wait_for_slug" an, damit die "my_instance_management"-Seite weiß, - # dass sie sofort ein Lade-Overlay für diesen spezifischen Tenant öffnen muss. flash(f"Buchung für {selected_package} wurde erfasst. Deine Instanz wird jetzt automatisch eingerichtet.", "success") return redirect(url_for("my_instance_management", wait_for_slug=subdomain)) diff --git a/Website/modules/emailservice/email.py b/Website/modules/emailservice/email.py index 88da174..2fc74f5 100644 --- a/Website/modules/emailservice/email.py +++ b/Website/modules/emailservice/email.py @@ -202,12 +202,11 @@ def generate_main_contract_pdf( software_name: str = "Invario Inventarsystem" ): """Generiert den Hauptvertrag mit zentrierter und aufeinander abgestimmter Kopf- und Fußzeilen-Formatierung.""" - from datetime import datetime pdf = FPDF() pdf.add_page() pdf.set_auto_page_break(auto=True, margin=15) - # --- KOPFBEREICH (ZENTRIERT & BÜNDIG UNTEREINANDER) --- + # --- KOPFBEREICH --- pdf.set_font("Helvetica", style="B", size=13) pdf.cell(0, 6, "Vertrag über die Nutzung der Software-Lizenz", ln=True, align="C") pdf.cell(0, 6, f'"{software_name}"', ln=True, align="C") @@ -231,7 +230,7 @@ def generate_main_contract_pdf( pdf.cell(0, 5, "- Auftraggeber / Schule -", ln=True, align="C") pdf.ln(8) - # --- PARAGRAPHEN (1 BIS 8) --- + # --- PARAGRAPHEN --- pdf.set_font("Helvetica", style="B", size=10) pdf.cell(0, 6, "1. Vertragsgegenstand", ln=True) pdf.set_font("Helvetica", size=10) @@ -309,28 +308,25 @@ def generate_main_contract_pdf( ) pdf.ln(6) - # --- FUSSBEREICH / UNTERSCHRIFTEN (ZENTRIERT & SYMMETRISCH) --- + # --- UNTERSCHRIFTEN --- pdf.set_font("Helvetica", style="B", size=10) pdf.cell(0, 6, "Unterschriften und Einverständnis", ln=True, align="C") pdf.set_font("Helvetica", size=9) pdf.multi_cell(0, 4, "Mit der Unterzeichnung bestätigen die Parteien den Abschluss des Hauptvertrages inklusive der beigefügten Anlagen (AGB, EULA und AVV).", align="C") pdf.ln(8) - # Exakte Zentrierung der zwei Unterschriften-Blöcke col_width = 85 - start_x = (210 - (col_width * 2)) / 2 # Berechnet den exakten linken Rand für Symmetrie (20mm) + start_x = (210 - (col_width * 2)) / 2 pdf.set_font("Helvetica", size=9) - pdf.set_x(start_x) pdf.cell(col_width, 5, "Für den Auftraggeber", align="C", ln=False) pdf.cell(col_width, 5, "Für den Anbieter", align="C", ln=True) - pdf.ln(8) pdf.set_x(start_x) - pdf.cell(col_width, 5, f"{datetime.today().strftime('%d.%m.%Y')} ________________________", align="C", ln=False) - pdf.cell(col_width, 5, f"{datetime.today().strftime('%d.%m.%Y')} ___________ _____________", align="C", ln=True) + pdf.cell(col_width, 5, f"{date} ________________________", align="C", ln=False) + pdf.cell(col_width, 5, f"{date} ___________ _____________", align="C", ln=True) pdf.set_x(start_x) pdf.cell(col_width, 5, "(Verantwortlicher)", align="C", ln=False) @@ -339,27 +335,31 @@ def generate_main_contract_pdf( pdf.output(output_path) return output_path - def send_accreditation_email( recipient: str, domain: str, username: str, password: str, - invoice_pdf_path: str, - contract_pdf_path: str, school_name: str = "[Name der Schule / des Auftraggebers]", address: str = "[Straße, Hausnummer] [PLZ, Ort]", price: str = "250,00", - date: str = "[Ort, Datum]" + date: str = "[Ort, Datum]", + software_name: str = "Invario Inventarsystem" ) -> bool: - """Generiert den Hauptvertrag und sendet diesen zusammen mit Rechnung, AGB und AVV per E-Mail.""" + """Generiert den Hauptvertrag temporär und versendet ihn zusammen mit AGB & AVV per E-Mail.""" - # 1. Hauptvertrag generieren + # Clean Sanitization für Dateinamen + safe_school_name = "".join([c for c in school_name if c.isalnum() or c in (' ', '_', '-')]).rstrip() + contract_filename = f"Hauptvertrag_Invario_{safe_school_name}.pdf" + contract_pdf_path = os.path.join(tempfile.gettempdir(), contract_filename) + + # 1. Hauptvertrag dynamisch erzeugen generate_main_contract_pdf( school_name=school_name, address=address, price=price, date=date, + software_name=software_name, output_path=contract_pdf_path ) @@ -367,13 +367,13 @@ def send_accreditation_email( agb_pdf_path = os.path.join("static", "download", "AGB Invario.pdf") avv_pdf_path = os.path.join("static", "download", "AVV Invario.pdf") - # 3. E-Mail Texte + # 3. E-Mail Inhalte subject = "Ihre Zugangsdaten und Unterlagen für Invario" text_note = ( f"Sehr geehrte/r {username},\n\n" "Vielen Dank für Ihre Akkreditierung bei Invario. " - "Im Anhang finden Sie Ihre Rechnung, den Hauptvertrag sowie die AGB und den AVV.\n\n" + "Im Anhang finden Sie Ihren Hauptvertrag sowie die AGB und den AVV.\n\n" f"Ihre Zugangsdaten:\n" f"Domain: https://{domain}\n" f"Benutzername: {username}\n" @@ -385,7 +385,7 @@ def send_accreditation_email(

Willkommen bei Invario!

- Vielen Dank für Ihre Akkreditierung. Im Anhang finden Sie Ihre Rechnung, den Hauptvertrag sowie die AGB und den AVV. + Vielen Dank für Ihre Akkreditierung. Im Anhang finden Sie Ihren Hauptvertrag sowie die AGB und den AVV.

Ihre Zugangsdaten:

@@ -401,25 +401,29 @@ def send_accreditation_email(
""" - # 4. Alle existierenden Anhänge sammeln + # 4. Vorhandene Anhänge einsammeln attachments = [] - # Generierter Hauptvertrag & Rechnung - if contract_pdf_path and os.path.exists(contract_pdf_path): + if os.path.exists(contract_pdf_path): attachments.append(contract_pdf_path) - if invoice_pdf_path and os.path.exists(invoice_pdf_path): - attachments.append(invoice_pdf_path) - - # Statische Dokumente aus static/download/ + if os.path.exists(agb_pdf_path): attachments.append(agb_pdf_path) else: - print(f"Warnung: AGB unter {agb_pdf_path} nicht gefunden.") + print(f"[WARNING] AGB unter {agb_pdf_path} nicht gefunden.") if os.path.exists(avv_pdf_path): attachments.append(avv_pdf_path) else: - print(f"Warnung: AVV unter {avv_pdf_path} nicht gefunden.") + print(f"[WARNING] AVV unter {avv_pdf_path} nicht gefunden.") - # 5. E-Mail mit allen 4 Anhängen versenden - return send(recipient, subject, text_body=text_note, html_body=html_note, attachments=attachments) \ No newline at end of file + # 5. E-Mail versenden & temporäre Datei aufräumen + try: + success = send(recipient, subject, text_body=text_note, html_body=html_note, attachments=attachments) + return success + finally: + if os.path.exists(contract_pdf_path): + try: + os.remove(contract_pdf_path) + except OSError as e: + print(f"[WARNING] Konnte temporären Vertrag nicht löschen: {e}") \ No newline at end of file