325 lines
12 KiB
Python
325 lines
12 KiB
Python
"""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)
|