Implementation of a Testversion

This commit is contained in:
2026-09-06 21:24:37 +02:00
parent 3ddef7b65f
commit 608759c0f9
3 changed files with 160 additions and 56 deletions
+11
View File
@@ -294,6 +294,17 @@ watcher_loop() {
echo "[ERROR] Fehler beim Neustart von: $SLUG" >&2 echo "[ERROR] Fehler beim Neustart von: $SLUG" >&2
fi 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 echo "[ERROR] Unbekannte Aktion ($ACTION) in $file" >&2
;; ;;
+111 -55
View File
@@ -52,6 +52,7 @@ BOOKING_PACKAGE_MAP = {
"terminverwaltung": "Terminverwaltung", "terminverwaltung": "Terminverwaltung",
"emailversand": "E-Mail Versand", "emailversand": "E-Mail Versand",
"schultraeger": "Schulträger Angebot", "schultraeger": "Schulträger Angebot",
"testversion": "Testversion (kostenlos)",
} }
BOOKING_FLOW_MAP = { BOOKING_FLOW_MAP = {
@@ -79,6 +80,7 @@ BOOKING_PRICE_MAP = {
"terminverwaltung": 100.0, "terminverwaltung": 100.0,
"emailversand": 50.0, "emailversand": 50.0,
"schultraeger": 0.0, "schultraeger": 0.0,
"testversion": 0.0,
} }
BOOKING_ADMIN_EMAIL_FALLBACK = os.environ.get("BOOKING_ADMIN_EMAIL", "").strip() 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)) 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 = { PACKAGE_MODULE_MAP = {
"buecherei": ["library"], "buecherei": ["library"],
"inventarsystem": ["inventory"], "inventarsystem": ["inventory"],
@@ -2187,6 +2189,7 @@ PACKAGE_MODULE_MAP = {
"startpaket": ["library", "inventory", "student_cards"], "startpaket": ["library", "inventory", "student_cards"],
"advanced": ["library", "inventory", "student_cards", "terminplan"], "advanced": ["library", "inventory", "student_cards", "terminplan"],
"komplettpaket": ["library", "inventory", "student_cards", "terminplan"], "komplettpaket": ["library", "inventory", "student_cards", "terminplan"],
"testversion": ["testversion", "Testversion"]
} }
@@ -2225,16 +2228,35 @@ def background_booking_provisioning(
username, username,
address, address,
price, 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(): with app_instance.app_context():
req_client, req_col = _get_collection("instance_requests") req_client, req_col = _get_collection("instance_requests")
try: try:
# 1. Status auf "in_progress" setzen # 1. Präfix für Testversionen verarbeiten
req_col.update_one({"_id": ObjectId(prov_id)}, {"$set": {"provision_status": "in_progress"}}) 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") inst_client_check, inst_col_check = _get_collection("instances")
try: try:
existing_instances = list(inst_col_check.find({}, {"https_port": 1, "port": 1})) 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 # Generiere den korrekten Modul-String
module_config_str = build_module_config_string(package_list) 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) 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 = { if is_trial:
"action": "add", trigger_payload = {
"request_id": str(prov_id), "action": "trial",
"slug": subdomain, "request_id": str(prov_id),
"port": next_port, "slug": effective_subdomain,
"password": admin_password, "port": next_port,
"module_config": module_config_str "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: with open(trigger_file, "w") as f:
json.dump(trigger_payload, f) json.dump(trigger_payload, f)
print(f"[INFO] Trigger-Datei für 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 # 6. Instanz-Eintrag in der Datenbank registrieren
domain = f"{subdomain}.invario-software.de" domain = f"{effective_subdomain}.invario-software.de"
instance_doc = { instance_doc = {
"owner_id": user_id, "owner_id": user_id,
"owner_username": username, "owner_username": username,
"school_name": school_name, "school_name": school_name,
"subdomain": subdomain, "subdomain": effective_subdomain,
"domain": domain, "domain": domain,
"https_port": next_port, "https_port": next_port,
"status": "ready", "status": "ready",
"nginx_status": "active", "nginx_status": "active",
"admin_username": "admin", "admin_username": "admin",
"admin_password": admin_password, "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") inst_client, inst_col = _get_collection("instances")
try: try:
@@ -2292,7 +2334,7 @@ def background_booking_provisioning(
finally: finally:
inst_client.close() inst_client.close()
# 5. Bestätigungs-E-Mail inkl. Vertrag & Anhängen versenden # 7. Bestätigungs-E-Mail versenden
try: try:
if user_email: if user_email:
formatted_date = datetime.today().strftime('%d.%m.%Y') formatted_date = datetime.today().strftime('%d.%m.%Y')
@@ -2304,29 +2346,34 @@ def background_booking_provisioning(
password=admin_password, password=admin_password,
school_name=school_name, school_name=school_name,
address=address, address=address,
price=price, price=price if not is_trial else "Kostenlose Testversion",
date=formatted_date, date=formatted_date,
software_name=software_name, software_name=f"{software_name} (Testversion)" if is_trial else software_name,
user_id=username user_id=username
) )
except Exception as mail_err: except Exception as mail_err:
print(f"[WARNING] E-Mail-Versand fehlgeschlagen: {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( req_col.update_one(
{"_id": ObjectId(prov_id)}, {"_id": ObjectId(prov_id)},
{"$set": { {"$set": 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()
}}
) )
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: except Exception as e:
print(f"[ERROR] Async Provisioning fehlgeschlagen für {subdomain}:") 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() 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() 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" booking_flow = "payment"
is_json_request = "application/json" in request.headers.get("Accept", "") or request.is_json 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) selected_package = ", ".join(selected_package_names)
package_raw = packages_str 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 = { form_data = {
"contact_person": _sanitize_text(request.values.get("contact_person") or "", 120), "contact_person": _sanitize_text(request.values.get("contact_person") or "", 120),
@@ -2462,8 +2512,9 @@ def booking_payment():
review_data = { review_data = {
"package": selected_package, "package": selected_package,
"package_key": package_raw, "package_key": package_raw,
"package_price": total_price, "package_price": total_price if not is_trial else 0.0,
"flow": booking_flow, "flow": booking_flow,
"is_trial": is_trial,
"title": flow_config["title"], "title": flow_config["title"],
"intro": flow_config["intro"], "intro": flow_config["intro"],
"confirm_label": flow_config["confirm_label"], "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 assigned_admin_email = (assigned_admin.get("email") if assigned_admin else "") or BOOKING_ADMIN_EMAIL_FALLBACK
try: try:
# FLOW 1: Consultation / Chat
if booking_flow == "consultation": if booking_flow == "consultation":
client, col = _get_collection("chat_messages") client, col = _get_collection("chat_messages")
try: try:
@@ -2541,12 +2591,13 @@ def booking_payment():
flash(f"Gesprächstermin für {selected_package} wurde an das Team gesendet.", "success") flash(f"Gesprächstermin für {selected_package} wurde an das Team gesendet.", "success")
return redirect(url_for("user_chat")) return redirect(url_for("user_chat"))
# FLOW 2: Direktbuchung mit Provisioning via Host-Watcher if booking_flow in ["payment", "trial"]:
if booking_flow == "payment": booking_number = f"{"TRIAL" if is_trial else "BOOK"}-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}"
booking_number = f"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 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] 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") inst_check_client, inst_col_check = _get_collection("instances")
try: try:
@@ -2558,14 +2609,14 @@ def booking_payment():
finally: finally:
inst_check_client.close() 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." err_msg = "Die gewünschte Subdomain existiert bereits. Bitte wähle eine andere Subdomain."
if is_json_request: if is_json_request:
return jsonify({"success": False, "error": err_msg}), 400 return jsonify({"success": False, "error": err_msg}), 400
flash(err_msg, "error") flash(err_msg, "error")
return render_template("schnell_buchung.html", **review_data) return render_template("schnell_buchung.html", **review_data)
subdomain = base[:63] subdomain = effective_subdomain[:63]
provision_payload = { provision_payload = {
"user_id": session.get("user_id"), "user_id": session.get("user_id"),
@@ -2573,9 +2624,10 @@ def booking_payment():
"booking_number": booking_number, "booking_number": booking_number,
"package": package_raw, "package": package_raw,
"package_label": selected_package, "package_label": selected_package,
"amount_eur": total_price, "amount_eur": total_price if not is_trial else 0.0,
"booking_flow": booking_flow, "booking_flow": booking_flow,
"booking_data": form_data, "booking_data": form_data,
"is_trial": is_trial,
"assigned_admin_username": assigned_admin_username, "assigned_admin_username": assigned_admin_username,
"assigned_admin_display_name": assigned_admin_display_name, "assigned_admin_display_name": assigned_admin_display_name,
"assigned_admin_email": assigned_admin_email, "assigned_admin_email": assigned_admin_email,
@@ -2597,15 +2649,14 @@ def booking_payment():
username = session.get("username") username = session.get("username")
user_email = session.get("email") or form_data.get("contact_email") 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']}" 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( thread = threading.Thread(
target=background_booking_provisioning, target=background_booking_provisioning,
args=( args=(
app_obj, app_obj,
prov_id, prov_id,
subdomain, base,
package_list, package_list,
user_id, user_id,
form_data["school_name"], form_data["school_name"],
@@ -2614,10 +2665,14 @@ def booking_payment():
address_process, address_process,
price_str, price_str,
selected_package, selected_package,
) ),
kwargs={
"is_trial": is_trial,
"trial_days": trial_days
}
) )
thread.start() 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: if is_json_request:
return jsonify({ return jsonify({
@@ -2672,7 +2727,6 @@ def instance_request_delete():
return redirect(url_for("my_invoices")) return redirect(url_for("my_invoices"))
@app.route('/admin/instances', methods=['GET']) @app.route('/admin/instances', methods=['GET'])
@admin_required @admin_required
def admin_instances(): def admin_instances():
@@ -3610,12 +3664,13 @@ def admin_delete_all_instances():
# Leite den Admin danach zurück zu einer sicheren Seite # Leite den Admin danach zurück zu einer sicheren Seite
return redirect(url_for('my_instance_management')) return redirect(url_for('my_instance_management'))
"""
@app.route('/test_email') @app.route('/test_email')
def test_email(): def test_email():
"""
Test endpoint to send a sample email. #Test endpoint to send a sample email.
This is for development purposes only. #This is for development purposes only.
"""
try: try:
send_accreditation_email( send_accreditation_email(
recipient="maximilian.gruendinger@invario-software.de", recipient="maximilian.gruendinger@invario-software.de",
@@ -3632,7 +3687,8 @@ def test_email():
return "Test email sent successfully." return "Test email sent successfully."
except Exception as e: except Exception as e:
return f"Failed to send test email: {str(e)}", 500 return f"Failed to send test email: {str(e)}", 500
"""
def main(): def main():
app.run(host="0.0.0.0", port=4999, debug=False) app.run(host="0.0.0.0", port=4999, debug=False)
+38 -1
View File
@@ -146,6 +146,34 @@
</section> </section>
<section class="packages-grid" aria-label="Buchungspakete"> <section class="packages-grid" aria-label="Buchungspakete">
<!-- TESTVERSION -->
<article class="package-card">
<div class="package-card__header">
<div>
<span class="package-card__tag" style="background: #e8f5e9; color: #2e7d32;">Kostenlos</span>
<h2 class="package-card__name">Testversion</h2>
</div>
<div class="package-card__price">
<span>0 €</span><small> -> 14 Tage</small>
</div>
</div>
<p class="package-card__description">Testen Sie alle Module (Inventar, Bibliothek & Terminplaner) 14 Tage lang unverbindlich in Ihrer Schule.</p>
<ul class="package-card__features">
<li>Voller Zugriff auf alle Basis-Module</li>
<li>Keine automatische Verlängerung</li>
<li>Vollständige Datenübernahme bei Kauf</li>
<li>Endet automatisch nach 14 Tagen</li>
</ul>
<div class="package-card__actions">
{% if 'username' in session %}
<button class="btn add-to-cart-btn" data-id="testversion" data-name="Testversion (14 Tage)" data-price="0" onclick="handleAddToCart(this)">
Jetzt kostenlos starten
</button>
{% else %}
<a class="btn secondary" href="{{ url_for('login') }}">Zum Testen einloggen</a>
{% endif %}
</div>
</article>
<!-- INVENTARSYSTEM --> <!-- INVENTARSYSTEM -->
<article class="package-card"> <article class="package-card">
<div class="package-card__header"> <div class="package-card__header">
@@ -383,7 +411,10 @@
if(item.price > 0) total += item.price; if(item.price > 0) total += item.price;
if(item.price === 0) hasIndividualPrice = true; 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 += ` container.innerHTML += `
<div class="cart-item"> <div class="cart-item">
@@ -414,6 +445,12 @@
return; 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 // Konflikte mit Startpaket abfangen
const hasStartpaket = cart.find(i => i.id === 'startpaket'); const hasStartpaket = cart.find(i => i.id === 'startpaket');
if (hasStartpaket && (id === 'inventarsystem' || id === 'buecherei')) { if (hasStartpaket && (id === 'inventarsystem' || id === 'buecherei')) {