Commit 674c4826 authored by Marco Schmiedel's avatar Marco Schmiedel

fix

parent 11270df4
......@@ -53,6 +53,7 @@ Deine Aufgabe ist es, beide Dokumente sorgfältig und vergleichend zu analysiere
26. **`pricing_monthly_after_period_eur_netto` (Number oder null):** Netto-Preis nach Aktionsphase = Brutto / 1.19.
27. **`contract_min_duration_months` (Number oder null):** Mindest-Vertragslaufzeit in Monaten.
28. **`contract_cancellation_notice_period_months` (Number oder null):** Kündigungsfrist in Monaten.
29. **`hardware_subsidy_eur_brutto` (5,10,15,20 oder 0):** Höhe der Hardwaresubvention (im Tarifname steht "mit Handy 5" = 5, mit Smartphone 5 = 5, mit Smartphone 10 = 10 usw.)
Werte, die nicht zuverlässig extrahiert werden können, auf `null` setzen.
Numerische Werte als Number belassen, Netto stets auf 4 Nachkommastellen runden.
......@@ -92,6 +93,7 @@ expectedKeys: List[str] = [
"pricing_monthly_after_period_eur_netto",
"contract_min_duration_months",
"contract_cancellation_notice_period_months",
"hardware_subsidy_eur_brutto",
]
......
......@@ -4,7 +4,7 @@ MYSQL_PASSWORD = "floz09sx3dTyx144gy"
MYSQL_DATABASE = "itmax_tarifs"
MYSQL_PORT = 3306
USE_SSH_TUNNEL = False
USE_SSH_TUNNEL = True
SSH_HOST = "jumphost.bugsmasher.online"
SSH_PORT = 22
SSH_USERNAME = "root"
......
from __future__ import annotations
import sys; sys.path.append("..")
import sys
from typing import Any, Dict, List, Tuple
from flask import Blueprint, jsonify, abort
from flask import abort, Blueprint, jsonify
from sqlalchemy.orm import Session
from sqlalchemy import func
sys.path.append("..")
from manager.MysqlManager import MysqlManager
from models.aiprice_aipr import AipriceAipr
from models.base_base import BaseBase
from models.deal_deal import DealDeal
from models.option_opti import OptionOpti
from models.aiprice_aipr import AipriceAipr
# The blueprint instance is created with the module name stripped of dots so
# registering it never triggers a ValueError.
# Die Blueprint-Instanz wird mit dem Modulnamen ohne Punkte erstellt, damit die Registrierung keinen ValueError auslöst.
blueprint = Blueprint(__name__.rsplit(".", 1)[-1], __name__)
# This function builds the full JSON response for a given base object id,
# returning None when the id does not exist.
# Diese Funktion erstellt die vollständige JSON-Antwort für eine gegebene Basisobjekt-ID und gibt None zurück, wenn die ID nicht existiert.
def _build_base_response(session: Session, base_id: int) -> Dict[str, Any] | None:
# This query loads the base record that matches the requested id or returns None when no match is found.
# Diese Abfrage lädt den Basisdatensatz, der der angeforderten ID entspricht, oder gibt `None` zurück, falls keine Übereinstimmung gefunden wird.
base_record: BaseBase | None = (
session.query(BaseBase)
.filter_by(id_base=base_id)
.one_or_none()
)
# This conditional branch exits early when the requested base id does not exist.
# Dieser Zweig der Bedingung sorgt für einen frühzeitigen Ausstieg, falls die angeforderte Basis-ID nicht existiert.
if base_record is None:
# Die Funktion gibt None zurück, da kein passender Datensatz gefunden wurde.
return None
# This query loads all deal records that belong to the current base object.
# Diese Abfrage lädt alle Deal-Datensätze, die zum aktuellen Basisobjekt gehören.
deal_records: List[DealDeal] = (
session.query(DealDeal)
.filter_by(base_deal=base_record.id_base)
.all()
)
# This query loads every option that belongs to the current base object.
# Diese Abfrage lädt jede Option, die zum aktuellen Basisobjekt gehört.
opti_records: List[OptionOpti] = (
session.query(OptionOpti)
.filter_by(base_opti=base_record.id_base)
.all()
)
# This set collects all deal and option names to fetch their corresponding AI prices in a single query.
# Dieses Set sammelt alle Deal- und Optionsnamen, um deren zugehörige KI-Preise in einer einzigen Abfrage abzurufen.
names_to_lookup = {d.name_deal for d in deal_records if d.name_deal} | {o.name_opti for o in opti_records if o.name_opti}
# This dictionary maps a name (key_aipr) to its AI-identified price JSON object.
# Dieses Dictionary bildet einen Namen (key_aipr) auf das zugehörige von der KI identifizierte Preis-JSON-Objekt ab.
aiprice_map: Dict[str, Any] = {}
# Die Bedingung prüft, ob Namen zum Nachschlagen vorhanden sind, um eine leere Datenbankabfrage zu vermeiden.
if names_to_lookup:
# Die Abfrage ruft alle relevanten KI-Preis-Datensätze aus der Datenbank ab.
aiprice_results = session.query(AipriceAipr).filter(AipriceAipr.key_aipr.in_(names_to_lookup)).all()
# Die Ergebnisse werden in das Dictionary für einen schnellen Zugriff umgewandelt.
aiprice_map = {r.key_aipr: r.response_aipr for r in aiprice_results}
# This expression copies the JSON details column or uses an empty dictionary when the column is NULL.
# Dieser Ausdruck kopiert die JSON-Spalte `details` oder verwendet ein leeres Dictionary, wenn die Spalte `NULL` ist.
details_data: Dict[str, Any] = (
base_record.details_base.copy()
if base_record.details_base else {}
)
# This list will hold the processed deals.
# In dieser Liste werden die verarbeiteten Deals gespeichert.
deals: List[Dict[str, Any]] = []
# Diese Schleife durchläuft alle gefundenen Deal-Datensätze, um sie für die JSON-Antwort aufzubereiten.
for d in deal_records:
# Ein Dictionary wird erstellt, um die Deal-Daten in einem strukturierten Format zu halten.
deal_dict = {
"id": d.id_deal,
"providercode": d.providercode_deal,
......@@ -76,46 +90,80 @@ def _build_base_response(session: Session, base_id: int) -> Dict[str, Any] | Non
"updated": d.updated_deal.isoformat() if d.updated_deal else None,
}
# Die KI-Parameter für den aktuellen Deal-Namen werden aus der Map abgerufen.
ai_params = aiprice_map.get(d.name_deal)
# Es wird geprüft, ob für den Deal KI-Parameter gefunden wurden.
if ai_params:
# Eine Kopie der Parameter wird erstellt, um das Original nicht zu verändern.
processed_params = ai_params.copy()
# KORRIGIERT: Prüft auf < 0, da Rabatte als negative Prozentzahlen von der KI geliefert werden.
# Der prozentuale Rabatt wird aus den Parametern extrahiert und entfernt.
discount_percent = processed_params.pop('discount_percent', 0) or 0
# Wenn der prozentuale Rabatt negativ ist, wird ein Euro-Rabatt berechnet.
if discount_percent < 0:
# Der monatliche Bruttopreis des Basistarifs wird abgerufen.
base_monthly_brutto = details_data.get('pricing_monthly_initial_eur_brutto', 0) or 0
processed_params['discount_euro_brutto'] = round((base_monthly_brutto / 100) * discount_percent, 4)
# If connection_fee_brutto is null, set to 0
# Die Hardwaresubvention wird aus den Tarifdetails geholt.
hardware_subsidy_brutto = details_data.get('hardware_subsidy_eur_brutto', 0) or 0
# Der Brutto-Rabatt in Euro wird auf Basis des Monatspreises und der Subvention berechnet.
processed_params['discount_euro_brutto'] = round(((base_monthly_brutto - hardware_subsidy_brutto) / 100) * discount_percent, 4)
# Falls die Anschlussgebühr `null` ist, wird sie auf `0` gesetzt.
if processed_params.get('connection_fee_brutto') is None:
# Der Wert für die Brutto-Anschlussgebühr wird auf 0 gesetzt.
processed_params['connection_fee_brutto'] = 0
# Calculate connection_fee_netto
# Wenn eine Brutto-Anschlussgebühr vorhanden ist, wird der Nettobetrag berechnet.
if processed_params.get('connection_fee_brutto') is not None:
# Der Bruttobetrag wird zur Berechnung zwischengespeichert.
fee_brutto = processed_params['connection_fee_brutto']
# Der Nettobetrag der Anschlussgebühr wird berechnet und hinzugefügt.
processed_params['connection_fee_netto'] = round(fee_brutto / 1.19, 4)
# Calculate discount_euro_netto from discount_euro_brutto
# Aus dem Brutto-Rabatt in Euro wird der entsprechende Nettobetrag abgeleitet.
if processed_params.get('discount_euro_brutto') is not None:
# Der Bruttorabatt wird für die anstehende Berechnung zwischengespeichert.
discount_brutto = processed_params['discount_euro_brutto']
# Der Netto-Rabatt in Euro wird berechnet und dem Dictionary hinzugefügt.
processed_params['discount_euro_netto'] = round(discount_brutto / 1.19, 4)
# Rename provision_netto to provision2_ai_check
# Der Schlüssel `provision_netto` wird zur besseren Verständlichkeit in `provision2_ai_check` umbenannt.
if 'provision_netto' in processed_params:
# Der alte Schlüssel wird entfernt und der Wert unter dem neuen Schlüssel gespeichert.
processed_params['provision2_ai_check'] = processed_params.pop('provision_netto')
# Die verarbeiteten KI-Parameter werden dem Deal-Dictionary hinzugefügt.
deal_dict['ai_parameters'] = processed_params
# Falls keine KI-Parameter gefunden wurden, wird der Wert auf `None` gesetzt.
else:
# Dem Deal-Dictionary wird `None` für die KI-Parameter zugewiesen.
deal_dict['ai_parameters'] = None
# Das fertig aufbereitete Deal-Dictionary wird der finalen Liste hinzugefügt.
deals.append(deal_dict)
# The two collections below hold option nodes and category nodes so we can easily assemble the option hierarchy.
# Die beiden nachfolgenden Sammlungen speichern Options- und Kategorieknoten, um die Optionshierarchie einfach zusammenzusetzen.
option_nodes: List[Tuple[Dict[str, Any], str | None]] = []
category_nodes: Dict[str, Dict[str, Any]] = {}
# This loop converts each option database record into a node dictionary and remembers its parent relationship.
# Diese Schleife wandelt jeden Options-Datenbankdatensatz in ein Knoten-Dictionary um und merkt sich dessen Eltern-Beziehung.
for o in opti_records:
# Für jede Option wird ein neues Dictionary als Knoten erstellt.
node: Dict[str, Any] = {
"id": o.id_opti,
"providercode": o.providercode_opti,
......@@ -132,74 +180,116 @@ def _build_base_response(session: Session, base_id: int) -> Dict[str, Any] | Non
"items": [],
}
# Die KI-Parameter für den aktuellen Optionsnamen werden abgerufen.
ai_params = aiprice_map.get(o.name_opti)
# Es wird geprüft, ob KI-Parameter für diese Option existieren.
if ai_params:
# Eine veränderbare Kopie der Parameter wird erstellt.
processed_params = ai_params.copy()
# Rename provision_netto to provision1_ai_check
# Der Schlüssel `provision_netto` wird aus Konsistenzgründen umbenannt.
if 'provision_netto' in processed_params:
# Der Wert wird unter dem neuen Schlüssel `provision1_ai_check` gespeichert.
processed_params['provision1_ai_check'] = processed_params.pop('provision_netto')
# KORRIGIERT: Prüft auf < 0, da Rabatte als negative Prozentzahlen von der KI geliefert werden.
# Ein negativer prozentualer Rabatt löst die Berechnung des Euro-Rabatts aus.
discount_percent = processed_params.pop('discount_percent', 0) or 0
if discount_percent < 0:
# Der monatliche Bruttopreis des Basistarifs wird als Berechnungsgrundlage geholt.
base_monthly_brutto = details_data.get('pricing_monthly_initial_eur_brutto', 0) or 0
processed_params['discount_euro_brutto'] = round((base_monthly_brutto / 100) * discount_percent, 4)
# If connection_fee_brutto is 0, use negative base value
# Die Hardwaresubvention wird aus den allgemeinen Tarifdetails abgerufen.
hardware_subsidy_brutto = details_data.get('hardware_subsidy_eur_brutto', 0) or 0
# Der finale Rabatt in Euro wird berechnet und den Parametern hinzugefügt.
processed_params['discount_euro_brutto'] = round(((base_monthly_brutto - hardware_subsidy_brutto) / 100) * discount_percent, 4)
# Wenn die Brutto-Anschlussgebühr `0` ist, wird stattdessen der negative Wert des Basistarifs verwendet.
if processed_params.get('connection_fee_brutto') == 0:
# Die Anschlussgebühr des Basistarifs wird abgerufen.
base_connection_fee = details_data.get('pricing_connection_fee_eur_brutto')
# Falls eine Basis-Anschlussgebühr existiert, wird diese negiert.
if base_connection_fee is not None:
# Der negierte Wert wird als Anschlussgebühr für die Option gesetzt.
processed_params['connection_fee_brutto'] = base_connection_fee * -1
# Calculate connection_fee_netto
# Sofern eine Brutto-Anschlussgebühr existiert, wird der Nettobetrag berechnet.
if processed_params.get('connection_fee_brutto') is not None:
# Der Bruttowert wird in einer Variable zwischengespeichert.
fee_brutto = processed_params['connection_fee_brutto']
# Der Nettobetrag wird berechnet und hinzugefügt.
processed_params['connection_fee_netto'] = round(fee_brutto / 1.19, 4)
# Calculate discount_euro_netto from discount_euro_brutto
# Der Netto-Rabatt wird aus dem Brutto-Rabatt berechnet, falls dieser vorhanden ist.
if processed_params.get('discount_euro_brutto') is not None:
# Der Bruttorabatt wird zur Berechnung ausgelesen.
discount_brutto = processed_params['discount_euro_brutto']
# Der Nettorabatt wird berechnet und in den Parametern gespeichert.
processed_params['discount_euro_netto'] = round(discount_brutto / 1.19, 4)
# Die aufbereiteten KI-Parameter werden dem Knoten hinzugefügt.
node['ai_parameters'] = processed_params
# Wenn keine KI-Parameter gefunden wurden, wird das ursprüngliche Preisfeld verwendet.
else:
# If no AI parameters are found, use the original price field.
# Der ursprüngliche Preis der Option wird als Fließkommazahl gespeichert.
node['price'] = float(o.price_opti)
# This conditional branch stores nodes whose provider code begins with “G” so children can later be attached to them.
# Diese Bedingung speichert Knoten, deren Provider-Code mit „G“ beginnt, damit ihnen später Kinder zugeordnet werden können.
if o.providercode_opti.startswith("G"):
# Der Knoten wird als potenzieller Elternknoten in der Kategorie-Map gespeichert.
category_nodes[o.providercode_opti] = node
# Each tuple in option_nodes keeps the node itself and the code of its parent option or group.
# Jedes Tupel in `option_nodes` enthält den Knoten selbst und den Code seiner übergeordneten Option oder Gruppe.
option_nodes.append((node, o.providercategory_opti))
# This dictionary collects nodes that have no valid parent so they can be returned as top‑level entries grouped by parent code.
# Dieses Dictionary sammelt Knoten ohne gültiges Elternelement, um sie als Einträge der obersten Ebene zurückzugeben.
root_nodes: Dict[str | None, List[Dict[str, Any]]] = {}
# This loop attaches every node either to its parent category or to the root collection when no suitable parent exists.
# Diese Schleife ordnet jeden Knoten entweder seiner übergeordneten Kategorie oder der Wurzelsammlung zu.
for node, parent_code in option_nodes:
# This branch attaches a node to its parent when the parent exists and has been recognised as a category node.
# Dieser Zweig fügt einen Knoten seinem Elternelement hinzu, wenn dieses existiert und als Kategorie erkannt wurde.
if parent_code and parent_code in category_nodes:
# Der aktuelle Knoten wird der "items"-Liste seines Elternknotens hinzugefügt.
category_nodes[parent_code]["items"].append(node)
# This branch stores nodes without a valid parent as top‑level entries under their parent code.
# Dieser Zweig speichert Knoten ohne gültiges Elternelement als Einträge der obersten Ebene unter ihrem Elter-Code.
else:
# Der Knoten wird der Liste unter seinem Elter-Code in den Wurzelknoten hinzugefügt.
root_nodes.setdefault(parent_code, []).append(node)
# This loop removes empty “items” lists from category nodes so the client does not receive useless empty arrays.
# Diese Schleife entfernt leere „items“-Listen von Kategorieknoten, damit der Client keine nutzlosen leeren Arrays erhält.
for n in category_nodes.values():
# Es wird geprüft, ob die "items"-Liste des Knotens leer ist.
if not n["items"]:
# Die leere "items"-Liste wird aus dem Knoten entfernt.
n.pop("items", None)
# This comprehension builds the final list of option groups ready to be embedded into the JSON response.
# Diese Comprehension erstellt die endgültige Liste der Optionsgruppen, die in die JSON-Antwort eingebettet werden soll.
options: List[Dict[str, Any]] = [
{"providercode": parent_code, "items": items}
for parent_code, items in root_nodes.items()
]
# This dictionary gathers the base table column values ready for JSON serialization.
# Dieses Dictionary sammelt die Spaltenwerte der Basistabelle für die JSON-Serialisierung.
base_data: Dict[str, Any] = {
"id": base_record.id_base,
"provider": base_record.provider_base,
......@@ -212,33 +302,34 @@ def _build_base_response(session: Session, base_id: int) -> Dict[str, Any] | Non
"updated": base_record.updated_base.isoformat() if base_record.updated_base else None,
}
# This line removes the internal tariff_name helper key so it can be returned as a dedicated attribute.
# Diese Zeile entfernt den internen Hilfsschlüssel `tariff_name`, damit er als dediziertes Attribut zurückgegeben werden kann.
ai_identified_name = details_data.pop("tariff_name", None)
# This dictionary merges the core base data with the JSON details and the AI‑identified name.
# Dieses Dictionary führt die Kerndaten des Basistarifs mit den JSON-Details und dem von der KI identifizierten Namen zusammen.
merged_base = {
**base_data,
**details_data,
"ai_identified_name": ai_identified_name,
}
# The function returns the assembled base, deal, and option data so the caller can serialize it to JSON.
# Die Funktion gibt die zusammengestellten Basis-, Deal- und Optionsdaten zurück, damit der Aufrufer sie in JSON serialisieren kann.
return {
"base": merged_base,
"deals": deals,
"options": options,
}
# This route returns an overview of every base tariff that currently has at
# least one active deal (stops_deal IS NULL).
# Diese Route gibt eine Übersicht über jeden Basistarif zurück, der derzeit mindestens einen aktiven Deal hat.
@blueprint.route("/base", methods=["GET"])
def base_overview():
# A new database session is opened through the MySQL manager.
# Eine neue Datenbanksitzung wird über den MySQL-Manager geöffnet.
session = MysqlManager().getSession()
# Der try-Block stellt sicher, dass die Datenbankressourcen korrekt behandelt werden.
try:
# This query selects the distinct base objects that have at least one active deal.
# Diese Abfrage wählt die eindeutigen Basisobjekte aus, die mindestens einen aktiven Deal haben.
query = (
session.query(
BaseBase.id_base.label("id"),
......@@ -253,7 +344,7 @@ def base_overview():
.order_by(BaseBase.provider_base.asc())
)
# This comprehension converts every result row into a plain dictionary ready for JSON serialization.
# Diese Comprehension wandelt jede Ergebniszeile in ein einfaches Dictionary um, das für die JSON-Serialisierung bereit ist.
records: List[Dict[str, Any]] = [
{
"id": row.id,
......@@ -265,31 +356,39 @@ def base_overview():
for row in query.all()
]
# The finally block guarantees that the session is always closed.
# Der finally-Block garantiert, dass die Sitzung immer geschlossen wird.
finally:
# Die Datenbanksitzung wird sicher geschlossen, um Ressourcen freizugeben.
session.close()
# The route returns the list of base objects as a JSON array.
# Die Route gibt die Liste der Basisobjekte als JSON-Array zurück.
return jsonify(records)
# This route returns a complete JSON structure for a single base object or raises a 404 error when the id does not exist.
# Diese Route gibt eine vollständige JSON-Struktur für ein einzelnes Basisobjekt zurück oder löst einen 404-Fehler aus.
@blueprint.route("/base/<int:id>", methods=["GET"])
def base_details(id: int):
# A new database session is opened through the MySQL manager.
# Eine neue Datenbanksitzung wird über den MySQL-Manager geöffnet.
session = MysqlManager().getSession()
# Die Aktionen werden in einem try-Block ausgeführt, um eine saubere Handhabung der Sitzung zu gewährleisten.
try:
# The helper function assembles the complete response structure or returns None when the id is unknown.
# Die Hilfsfunktion stellt die vollständige Antwortstruktur zusammen oder gibt `None` zurück, wenn die ID unbekannt ist.
data = _build_base_response(session, id)
# The finally block guarantees that the session is always closed.
# Der finally-Block stellt sicher, dass die Sitzung auch bei Fehlern geschlossen wird.
finally:
# Die Sitzung wird geschlossen, um die Datenbankverbindung freizugeben.
session.close()
# This conditional branch aborts with a 404 status when the requested base id was not found.
# Dieser Zweig bricht mit einem 404-Status ab, wenn die angeforderte Basis-ID nicht gefunden wurde.
if data is None:
# Ein 404-Fehler wird ausgelöst, da das angeforderte Objekt nicht existiert.
abort(404, description=f"Base object with id={id} not found.")
# The route returns the fully assembled JSON structure for the requested base id.
# Die Route gibt die vollständig zusammengestellte JSON-Struktur für die angeforderte Basis-ID zurück.
return jsonify(data)
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment