From 608759c0f9ade61f94860f8574faa4ad391ff708 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Sun, 6 Sep 2026 21:24:37 +0200 Subject: [PATCH] Implementation of a Testversion --- Website/launch.sh | 11 +++ Website/main.py | 166 +++++++++++++++++++++++----------- Website/templates/preise.html | 39 +++++++- 3 files changed, 160 insertions(+), 56 deletions(-) diff --git a/Website/launch.sh b/Website/launch.sh index cd710aa..65f937d 100755 --- a/Website/launch.sh +++ b/Website/launch.sh @@ -294,6 +294,17 @@ watcher_loop() { echo "[ERROR] Fehler beim Neustart von: $SLUG" >&2 fi ;; + "trial") + DAYS=$(jq -r '.days // 14' "$file" 2>/dev/null || echo "14") + ACTUAL_PORT=$(get_free_port "$PORT") + + echo "[INFO] Erstelle Testversion für $SLUG (Port: $ACTUAL_PORT, Laufzeit: $DAYS Tage)..." + if (cd "$TARGET_SYSTEM_DIR" && "$TARGET_SCRIPT" trial "$SLUG" "$ACTUAL_PORT" "$DAYS"); then + echo "[SUCCESS] Testversion erfolgreich eingerichtet: $SLUG" + else + echo "[ERROR] Fehler beim Erstellen der Testversion für $SLUG" >&2 + fi + ;; *) echo "[ERROR] Unbekannte Aktion ($ACTION) in $file" >&2 ;; diff --git a/Website/main.py b/Website/main.py index 56979eb..7f1b27a 100644 --- a/Website/main.py +++ b/Website/main.py @@ -52,6 +52,7 @@ BOOKING_PACKAGE_MAP = { "terminverwaltung": "Terminverwaltung", "emailversand": "E-Mail Versand", "schultraeger": "Schulträger Angebot", + "testversion": "Testversion (kostenlos)", } BOOKING_FLOW_MAP = { @@ -79,6 +80,7 @@ BOOKING_PRICE_MAP = { "terminverwaltung": 100.0, "emailversand": 50.0, "schultraeger": 0.0, + "testversion": 0.0, } BOOKING_ADMIN_EMAIL_FALLBACK = os.environ.get("BOOKING_ADMIN_EMAIL", "").strip() @@ -2178,7 +2180,7 @@ def book_option_package(): return redirect(url_for("booking_payment", packages=packages_raw, flow=booking_flow)) -ALL_MODULE_KEYS = ["library", "inventory", "student_cards", "terminplan"] +ALL_MODULE_KEYS = ["library", "inventory", "student_cards", "terminplan", "testversion"] PACKAGE_MODULE_MAP = { "buecherei": ["library"], "inventarsystem": ["inventory"], @@ -2187,6 +2189,7 @@ PACKAGE_MODULE_MAP = { "startpaket": ["library", "inventory", "student_cards"], "advanced": ["library", "inventory", "student_cards", "terminplan"], "komplettpaket": ["library", "inventory", "student_cards", "terminplan"], + "testversion": ["testversion", "Testversion"] } @@ -2225,16 +2228,35 @@ def background_booking_provisioning( username, address, price, - software_name="Invario Inventarsystem" + software_name="Invario Inventarsystem", + is_trial=False, # Neu: Flag für Testversion + trial_days=14, # Neu: Laufzeit der Testversion in Tagen + trial_prefix="testversion-" # Neu: Präfix für Testversionen ): - """Hintergrund-Worker, der die Tenant-Bereitstellung über das Host-Watcher-Triggersystem auslöst.""" + """ + Hintergrund-Worker, der die Tenant-Bereitstellung über das Host-Watcher-Triggersystem auslöst. + Unterstützt reguläre Buchungen sowie Testversionen (mit 'testversion-' Präfix & Trial-Laufzeit). + """ with app_instance.app_context(): req_client, req_col = _get_collection("instance_requests") try: - # 1. Status auf "in_progress" setzen - req_col.update_one({"_id": ObjectId(prov_id)}, {"$set": {"provision_status": "in_progress"}}) + # 1. Präfix für Testversionen verarbeiten + effective_subdomain = subdomain + if is_trial: + if not effective_subdomain.startswith(trial_prefix): + effective_subdomain = f"{trial_prefix}{effective_subdomain}" + + # 2. Status auf "in_progress" setzen + req_col.update_one( + {"_id": ObjectId(prov_id)}, + {"$set": { + "provision_status": "in_progress", + "is_trial": is_trial, + "effective_subdomain": effective_subdomain + }} + ) - # 2. Nächsten Port und Admin-Passwort ermitteln + # 3. 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})) @@ -2252,39 +2274,59 @@ def background_booking_provisioning( # Generiere den korrekten Modul-String module_config_str = build_module_config_string(package_list) - # 3. Trigger-Datei für den Host-Watcher schreiben + # 4. Ablaufdatum berechnen (bei Testversion) + now = datetime.now(timezone.utc) + expires_at = (now + timedelta(days=trial_days)).isoformat() if is_trial else None + + # 5. 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") + trigger_file = os.path.join(TRIGGER_DIR, f"{effective_subdomain}.json") - trigger_payload = { - "action": "add", - "request_id": str(prov_id), - "slug": subdomain, - "port": next_port, - "password": admin_password, - "module_config": module_config_str - } + if is_trial: + trigger_payload = { + "action": "trial", + "request_id": str(prov_id), + "slug": effective_subdomain, + "port": next_port, + "days": trial_days, + "password": admin_password, + "module_config": module_config_str + } + else: + trigger_payload = { + "action": "add", + "request_id": str(prov_id), + "slug": effective_subdomain, + "port": next_port, + "password": admin_password, + "module_config": module_config_str + } with open(trigger_file, "w") as f: json.dump(trigger_payload, f) 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" + # 6. Instanz-Eintrag in der Datenbank registrieren + domain = f"{effective_subdomain}.invario-software.de" instance_doc = { "owner_id": user_id, "owner_username": username, "school_name": school_name, - "subdomain": subdomain, + "subdomain": effective_subdomain, "domain": domain, "https_port": next_port, "status": "ready", "nginx_status": "active", "admin_username": "admin", "admin_password": admin_password, - "created_at": _utc_now_iso() + "created_at": _utc_now_iso(), + "is_trial": is_trial } + + if is_trial: + instance_doc["trial_days"] = trial_days + instance_doc["expires_at"] = expires_at inst_client, inst_col = _get_collection("instances") try: @@ -2292,7 +2334,7 @@ def background_booking_provisioning( finally: inst_client.close() - # 5. Bestätigungs-E-Mail inkl. Vertrag & Anhängen versenden + # 7. Bestätigungs-E-Mail versenden try: if user_email: formatted_date = datetime.today().strftime('%d.%m.%Y') @@ -2304,29 +2346,34 @@ def background_booking_provisioning( password=admin_password, school_name=school_name, address=address, - price=price, + price=price if not is_trial else "Kostenlose Testversion", date=formatted_date, - software_name=software_name, + software_name=f"{software_name} (Testversion)" if is_trial else software_name, user_id=username ) except Exception as mail_err: print(f"[WARNING] E-Mail-Versand fehlgeschlagen: {mail_err}") - # 6. Status in instance_requests auf "ready" setzen + # 8. Status in instance_requests auf "ready" setzen + update_fields = { + "provision_status": "ready", + "provision_port": next_port, + "instance_id": created_inst_id, + "url": f"https://{domain}", + "username": "admin", + "password": admin_password, + "updated_at": _utc_now_iso(), + "is_trial": is_trial + } + if is_trial: + update_fields["expires_at"] = expires_at + req_col.update_one( {"_id": ObjectId(prov_id)}, - {"$set": { - "provision_status": "ready", - "provision_port": next_port, - "instance_id": created_inst_id, - "url": f"https://{domain}", - "username": "admin", - "password": admin_password, - "updated_at": _utc_now_iso() - }} + {"$set": update_fields} ) - print(f"[SUCCESS] Provisioning-Trigger erfolgreich gesendet für {subdomain}") + print(f"[SUCCESS] Provisioning-Trigger erfolgreich gesendet für {effective_subdomain}") except Exception as e: print(f"[ERROR] Async Provisioning fehlgeschlagen für {subdomain}:") @@ -2408,7 +2455,10 @@ def booking_payment(): packages_str = _sanitize_text(request.values.get("packages") or request.values.get("package") or "", 200).lower() booking_flow = _sanitize_text(request.values.get("flow") or "payment", 24).lower() - if booking_flow not in BOOKING_FLOW_MAP: + is_trial = (booking_flow == "trial") or request.values.get("is_trial", "").strip().lower() in {"1", "on", "true", "yes"} + trial_days = int(request.values.get("trial_days") or 14) + + if booking_flow not in BOOKING_FLOW_MAP and booking_flow != "trial": booking_flow = "payment" is_json_request = "application/json" in request.headers.get("Accept", "") or request.is_json @@ -2438,7 +2488,7 @@ def booking_payment(): selected_package = ", ".join(selected_package_names) package_raw = packages_str - flow_config = BOOKING_FLOW_MAP[booking_flow] + flow_config = BOOKING_FLOW_MAP.get(booking_flow, BOOKING_FLOW_MAP.get("payment")) form_data = { "contact_person": _sanitize_text(request.values.get("contact_person") or "", 120), @@ -2462,8 +2512,9 @@ def booking_payment(): review_data = { "package": selected_package, "package_key": package_raw, - "package_price": total_price, + "package_price": total_price if not is_trial else 0.0, "flow": booking_flow, + "is_trial": is_trial, "title": flow_config["title"], "intro": flow_config["intro"], "confirm_label": flow_config["confirm_label"], @@ -2516,7 +2567,6 @@ def booking_payment(): assigned_admin_email = (assigned_admin.get("email") if assigned_admin else "") or BOOKING_ADMIN_EMAIL_FALLBACK try: - # FLOW 1: Consultation / Chat if booking_flow == "consultation": client, col = _get_collection("chat_messages") try: @@ -2541,12 +2591,13 @@ 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: Direktbuchung mit Provisioning via Host-Watcher - if booking_flow == "payment": - booking_number = f"BOOK-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}" + if booking_flow in ["payment", "trial"]: + booking_number = f"{"TRIAL" if is_trial else "BOOK"}-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}" 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] + + effective_subdomain = f"testversion-{base}" if (is_trial and not base.startswith("testversion-")) else base inst_check_client, inst_col_check = _get_collection("instances") try: @@ -2558,14 +2609,14 @@ def booking_payment(): finally: inst_check_client.close() - if base in existing_subdomains: + if effective_subdomain in existing_subdomains: err_msg = "Die gewünschte Subdomain existiert bereits. Bitte wähle eine andere Subdomain." if is_json_request: return jsonify({"success": False, "error": err_msg}), 400 flash(err_msg, "error") return render_template("schnell_buchung.html", **review_data) - subdomain = base[:63] + subdomain = effective_subdomain[:63] provision_payload = { "user_id": session.get("user_id"), @@ -2573,9 +2624,10 @@ def booking_payment(): "booking_number": booking_number, "package": package_raw, "package_label": selected_package, - "amount_eur": total_price, + "amount_eur": total_price if not is_trial else 0.0, "booking_flow": booking_flow, "booking_data": form_data, + "is_trial": is_trial, "assigned_admin_username": assigned_admin_username, "assigned_admin_display_name": assigned_admin_display_name, "assigned_admin_email": assigned_admin_email, @@ -2597,15 +2649,14 @@ 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('.', ',') + price_str = f"{total_price:.2f}".replace('.', ',') if not is_trial else "Kostenlose Testversion" - # Startet den asynchronen Provisioning-Thread mit exakten Parametern thread = threading.Thread( target=background_booking_provisioning, args=( app_obj, prov_id, - subdomain, + base, package_list, user_id, form_data["school_name"], @@ -2614,10 +2665,14 @@ def booking_payment(): address_process, price_str, selected_package, - ) + ), + kwargs={ + "is_trial": is_trial, + "trial_days": trial_days + } ) thread.start() - print(f"[INFO] Background Provisioning Trigger gestartet für Subdomain: {subdomain}") + print(f"[INFO] Background Provisioning Trigger gestartet für Subdomain: {subdomain} (Trial: {is_trial})") if is_json_request: return jsonify({ @@ -2672,7 +2727,6 @@ def instance_request_delete(): return redirect(url_for("my_invoices")) - @app.route('/admin/instances', methods=['GET']) @admin_required def admin_instances(): @@ -3610,12 +3664,13 @@ def admin_delete_all_instances(): # Leite den Admin danach zurück zu einer sicheren Seite return redirect(url_for('my_instance_management')) +""" @app.route('/test_email') def test_email(): - """ - Test endpoint to send a sample email. - This is for development purposes only. - """ + + #Test endpoint to send a sample email. + #This is for development purposes only. + try: send_accreditation_email( recipient="maximilian.gruendinger@invario-software.de", @@ -3632,7 +3687,8 @@ def test_email(): return "Test email sent successfully." except Exception as e: return f"Failed to send test email: {str(e)}", 500 - +""" + def main(): app.run(host="0.0.0.0", port=4999, debug=False) diff --git a/Website/templates/preise.html b/Website/templates/preise.html index add2f74..465f1ed 100644 --- a/Website/templates/preise.html +++ b/Website/templates/preise.html @@ -146,6 +146,34 @@
+ +
+
+
+ Kostenlos +

Testversion

+
+
+ 0 € -> 14 Tage +
+
+

Testen Sie alle Module (Inventar, Bibliothek & Terminplaner) 14 Tage lang unverbindlich in Ihrer Schule.

+
    +
  • Voller Zugriff auf alle Basis-Module
  • +
  • Keine automatische Verlängerung
  • +
  • Vollständige Datenübernahme bei Kauf
  • +
  • Endet automatisch nach 14 Tagen
  • +
+
+ {% if 'username' in session %} + + {% else %} + Zum Testen einloggen + {% endif %} +
+
@@ -383,7 +411,10 @@ if(item.price > 0) total += item.price; if(item.price === 0) hasIndividualPrice = true; - const priceText = item.price > 0 ? `${item.price} €` : "Auf Anfrage"; + let priceText = `${item.price} €`; + if (item.price === 0) { + priceText = item.id === 'testversion' ? 'Kostenlos' : 'Auf Anfrage'; + } container.innerHTML += `
@@ -414,6 +445,12 @@ return; } + const hasTestversion = cart.find(i => i.id === 'testversion'); + if (hasTestversion || (id === 'testversion' && cart.length > 0)) { + alert('Die Testversion beinhaltet bereits alle Module und kann nicht mit anderen Paketen kombiniert werden. Bitte leeren Sie Ihren Warenkorb zuerst.'); + return; + } + // Konflikte mit Startpaket abfangen const hasStartpaket = cart.find(i => i.id === 'startpaket'); if (hasStartpaket && (id === 'inventarsystem' || id === 'buecherei')) {