"""Demo „Bestellservice“: kleiner Python-Dienst, Bestelldaten 2026, Projektunterlagen. Bewusst eingebaut: Der Code rechnet noch mit der alten Rabattregel, das Protokoll von vor zehn Tagen hat eine neue beschlossen. Daran sieht man, wie die KI Quellen verknüpft und eine Änderung vorschlägt. """ import csv import io import random from datetime import date, timedelta from app.demo import Demo, day, shop_db NAME = "Demo: Bestellservice" DESCRIPTION = ("Beispiel-Workspace mit dem Repository eines kleinen Bestellservice, Bestelldaten 2026 und " "Projektunterlagen (Protokolle, Wartungsvertrag, Betriebshandbuch).") EXAMPLES = [ "Wie ist der Bestellservice aufgebaut? Gib mir einen Überblick über die wichtigsten Module.", "Was hat sich in den letzten zwei Wochen im Repository geändert?", "Welche Region hatte 2026 den höchsten Umsatz? Zeig den Umsatz je Monat und Region als Diagramm.", "Welche Reaktionszeit sieht der Wartungsvertrag bei einer Störung mit Priorität 1 vor, und bis wann " "können wir kündigen?", "Passt die Rabattberechnung im Code zu dem, was in der Abstimmung zu den Rabatten beschlossen wurde?", "Setze die neue Rabattregel aus dem Protokoll im Code um und passe die Tests an.", ] # --- Repository: (Tage zurück, Autor, Nachricht, Dateien) ------------------------------------------------------- README = """# Bestellservice Kleiner Dienst für Bestellungen: Preise, Versandkosten, Rabatte und eine schlanke API. ## Aufbau - `bestellservice/bestellung.py`: Bestellung und Positionen, Gesamtpreis - `bestellservice/versand.py`: Versandkosten - `bestellservice/rabatt.py`: Rabattstaffel - `bestellservice/api.py`: Handler für Anlegen, Abrufen und Stornieren - `tests/`: Tests mit pytest """ BETRIEB = """ ## Betrieb - Deployment über die Pipeline `deploy-bestellservice`, Freigabe durch den Fachbereich. - Logs im zentralen Logging unter `app=bestellservice`. - Störungen: siehe Betriebshandbuch in den Projektunterlagen. """ BESTELLUNG = '''"""Bestellung mit Positionen; Preise in Cent, damit nicht gerundet werden muss.""" from dataclasses import dataclass, field from bestellservice.rabatt import rabatt_prozent from bestellservice.versand import versandkosten @dataclass class Position: artikel: str menge: int einzelpreis: int # Cent @dataclass class Bestellung: nummer: str kunde: str positionen: list[Position] = field(default_factory=list) storniert: bool = False def warenwert(self) -> int: return sum(p.menge * p.einzelpreis for p in self.positionen) def gesamtpreis(self) -> int: """Warenwert abzüglich Rabatt, zuzüglich Versand.""" wert = self.warenwert() rabatt = wert * rabatt_prozent(wert) // 100 return wert - rabatt + versandkosten(wert) ''' BESTELLUNG_START = '''"""Bestellung mit Positionen; Preise in Cent, damit nicht gerundet werden muss.""" from dataclasses import dataclass, field @dataclass class Position: artikel: str menge: int einzelpreis: int # Cent @dataclass class Bestellung: nummer: str kunde: str positionen: list[Position] = field(default_factory=list) def warenwert(self) -> int: return sum(p.menge * p.einzelpreis for p in self.positionen) def gesamtpreis(self) -> int: return self.warenwert() ''' VERSAND = '''"""Versandkosten: pauschal, ab einem Warenwert von 50 Euro frei.""" PAUSCHALE = 495 # Cent FREI_AB = 5000 # Cent def versandkosten(warenwert: int) -> int: return 0 if warenwert >= FREI_AB else PAUSCHALE ''' RABATT = '''"""Rabattstaffel nach Warenwert (in Cent).""" # Ab 500 Euro Warenwert 10 Prozent Rabatt SCHWELLE = 50000 PROZENT = 10 def rabatt_prozent(warenwert: int) -> int: return PROZENT if warenwert >= SCHWELLE else 0 ''' API = '''"""Handler der API; das Web-Framework ruft sie mit bereits geprüften Daten auf.""" from bestellservice.bestellung import Bestellung, Position BESTELLUNGEN: dict[str, Bestellung] = {} def anlegen(nummer: str, kunde: str, positionen: list[dict]) -> dict: bestellung = Bestellung(nummer, kunde, [Position(**p) for p in positionen]) BESTELLUNGEN[nummer] = bestellung return {"nummer": nummer, "gesamtpreis": bestellung.gesamtpreis()} def abrufen(nummer: str) -> dict: bestellung = BESTELLUNGEN[nummer] return {"nummer": nummer, "kunde": bestellung.kunde, "gesamtpreis": bestellung.gesamtpreis(), "storniert": bestellung.storniert} ''' API_STORNO = API + ''' def stornieren(nummer: str) -> dict: """Nur nicht bereits stornierte Bestellungen; Erstattung übernimmt die Buchhaltung.""" bestellung = BESTELLUNGEN[nummer] if bestellung.storniert: raise ValueError(f"Bestellung {nummer} ist bereits storniert.") bestellung.storniert = True return {"nummer": nummer, "storniert": True} ''' TEST_RABATT = '''from bestellservice.rabatt import rabatt_prozent def test_kein_rabatt_unter_der_schwelle(): assert rabatt_prozent(49999) == 0 def test_rabatt_ab_500_euro(): assert rabatt_prozent(50000) == 10 ''' TEST_VERSAND = '''from bestellservice.versand import versandkosten def test_versand_frei_ab_50_euro(): assert versandkosten(5000) == 0 assert versandkosten(4999) == 495 ''' AGENTS = """# Projektregeln - Python 3.12, Typ-Hinweise überall, keine neuen Abhängigkeiten ohne Absprache. - Beträge immer in Cent (int), nie als float. - Jede fachliche Regel hat einen Test unter `tests/`. - Commit-Nachrichten auf Deutsch, im Imperativ ("Rabatt anpassen"). """ PYPROJECT = """[project] name = "bestellservice" version = "0.3.0" requires-python = ">=3.12" [tool.pytest.ini_options] testpaths = ["tests"] """ GUTSCHEIN = '''"""Gutscheine (in Arbeit): fester Betrag in Cent, einmal einlösbar.""" EINGELOEST: set[str] = set() def einloesen(code: str, betrag: int) -> int: if code in EINGELOEST: raise ValueError("Gutschein bereits eingelöst.") EINGELOEST.add(code) return betrag ''' COMMITS = [ (21, "entwicklung", "Grundgerüst Bestellservice", { "README.md": README, "pyproject.toml": PYPROJECT, "bestellservice/__init__.py": "", "bestellservice/bestellung.py": BESTELLUNG_START, "bestellservice/api.py": API}), (18, "fachbereich", "Versandkosten: frei ab 50 Euro", { "bestellservice/versand.py": VERSAND, "tests/test_versand.py": TEST_VERSAND}), (12, "entwicklung", "Rabattstaffel einführen", { "bestellservice/rabatt.py": RABATT, "tests/test_rabatt.py": TEST_RABATT, "bestellservice/bestellung.py": BESTELLUNG.replace(" storniert: bool = False\n", "")}), (6, "admin", "Projektregeln in AGENTS.md festhalten", {"AGENTS.md": AGENTS}), (3, "entwicklung", "API: Bestellung stornieren", { "bestellservice/api.py": API_STORNO, "bestellservice/bestellung.py": BESTELLUNG}), (1, "fachbereich", "README: Betrieb ergänzen", {"README.md": README + BETRIEB}), ] BRANCH = ("feature/gutscheine", 2, "entwicklung", "Gutscheine: erster Entwurf", {"bestellservice/gutschein.py": GUTSCHEIN}) # --- Projektunterlagen (Pfad, Text) ----------------------------------------------------------------------------- # Projektstart vor dem ersten Commit, Rabatt-Abstimmung nach dem Commit mit der alten Staffel START, RABATTE = day(25), day(10) DOCUMENTS = { f"Protokolle/{RABATTE:%Y-%m-%d} Abstimmung Rabatte.md": f"""# Abstimmung Rabatte, {RABATTE:%d.%m.%Y} Teilnehmende: Fachbereich Vertrieb, Entwicklung, Controlling ## Beschlüsse 1. Die bisherige Staffel (10 % ab 500 Euro Warenwert) entfällt, weil sie die Marge zu stark drückt. 2. Neu gilt ab sofort: **5 % Rabatt ab 1.000 Euro Warenwert**. Darunter kein Rabatt. 3. Versandkosten bleiben unverändert (frei ab 50 Euro). 4. Die Entwicklung setzt die Regel innerhalb von zwei Wochen um, mit Tests für die Grenzwerte. ## Offene Punkte - Gutscheine: Entwurf liegt im Branch `feature/gutscheine`, Abstimmung im November. """, f"Protokolle/{START:%Y-%m-%d} Projektstart.md": f"""# Projektstart Bestellservice, {START:%d.%m.%Y} - Ziel: Bestellungen aus dem Webshop und von der Hotline in einem Dienst bündeln. - Beträge werden durchgängig in Cent geführt. - Betrieb über den bestehenden Wartungsvertrag (Rahmenvertrag Anwendungsbetrieb). - Nächster Termin: Abstimmung Rabatte am {RABATTE:%d.%m.} """, "Wartungsvertrag.md": """# Wartungsvertrag Anwendungsbetrieb (Auszug) ## § 3 Reaktionszeiten | Priorität | Beschreibung | Reaktionszeit | Wiederherstellung | |---|---|---|---| | 1 | Dienst nicht verfügbar | 2 Stunden, rund um die Uhr | 8 Stunden | | 2 | Wesentliche Funktion gestört | 8 Stunden an Werktagen | 3 Werktage | | 3 | Sonstige Störung | 2 Werktage | nach Absprache | ## § 7 Laufzeit und Kündigung Der Vertrag läuft bis zum 31.12.2027 und verlängert sich danach jeweils um ein Jahr. Er kann mit einer Frist von **drei Monaten zum Quartalsende** gekündigt werden, erstmals zum 31.12.2027 (Kündigung also spätestens am 30.09.2027). """, "Betriebshandbuch.md": """# Betriebshandbuch Bestellservice ## Deployment Über die Pipeline `deploy-bestellservice`. Produktion nur nach Freigabe durch den Fachbereich, nie freitags nach 14 Uhr. ## Überwachung - Health-Check: `/healthz`, alle 30 Sekunden. - Alarm, wenn mehr als 2 % der Bestellungen in 5 Minuten fehlschlagen. ## Störungen 1. Priorität nach Wartungsvertrag § 3 einstufen. 2. Priorität 1: Rufbereitschaft der Entwicklung über die Leitstelle alarmieren. 3. Ticket im Service-Desk anlegen, Ursache im Nachgang dokumentieren. """, "Onboarding.md": """# Onboarding im Team Bestellservice - Zugang zum Repository über das Team „Bestellservice“. - Projektregeln stehen in `AGENTS.md` im Repository. - Fragen zu Fachlichkeit: Fachbereich Vertrieb; zu Betrieb: Betriebshandbuch. """, } # --- Bestelldaten ----------------------------------------------------------------------------------------------- PRODUCTS = {"Kaffeemaschine": 24900, "Wasserkocher": 3990, "Mühle": 8990, "Milchaufschäumer": 5490, "Kaffeebohnen 1 kg": 1890, "Entkalker": 790} REGIONS = {"Nord": 1.0, "Süd": 1.25, "West": 1.1, "Ost": 0.8} CHANNELS = ["Online", "Online", "Online", "Filiale", "Telefon"] def orders_csv(rows: int = 1200) -> bytes: """Bestellungen 2026 (Januar bis Oktober), reproduzierbar; Süd wächst über das Jahr am stärksten.""" rng = random.Random(2026) out = io.StringIO() writer = csv.writer(out) writer.writerow(["bestellnr", "datum", "region", "kanal", "produkt", "menge", "einzelpreis_eur", "rabatt_prozent", "umsatz_eur", "retoure"]) start = date(2026, 1, 1) for n in range(rows): day = start + timedelta(days=rng.randrange(0, 297)) region = rng.choices(list(REGIONS), weights=[REGIONS[r] * (1 + (day.month / 10 if r == "Süd" else 0)) for r in REGIONS])[0] product = rng.choice(list(PRODUCTS)) quantity = rng.choices([1, 2, 3, 5, 10], weights=[60, 20, 10, 6, 4])[0] price = PRODUCTS[product] value = price * quantity discount = 10 if value >= 50000 else 0 revenue = value * (100 - discount) / 100 / 100 writer.writerow([f"B-2026-{n + 1:05d}", day.isoformat(), region, rng.choice(CHANNELS), product, quantity, f"{price / 100:.2f}", discount, f"{revenue:.2f}", "ja" if rng.random() < 0.04 else "nein"]) return out.getvalue().encode() DEMO = Demo(key="bestellservice", name=NAME, description=DESCRIPTION, examples=EXAMPLES, repo="bestellservice", commits=COMMITS, branch=BRANCH, upload=("bestellungen_2026.csv", orders_csv), folder="Projektunterlagen", documents=DOCUMENTS, extra=shop_db.setup)