"""Coaching method: cost = Σ(lesson_type_price × hours) over a lesson plan. Base lesson-type prices reproduce real quotes (GA4/GTM 중급 → ₩1,570,000). Optional matrix mode prices by subject level (base × level_multiple). Student-count discount is opt-in (real 1:1 quotes apply none). """ def _discount(rate, students, apply): if not apply: return 0.0 for mx, r in rate["coaching"]["student_discount"]["bands"]: if students <= mx: return float(r) return 0.0 # 30+ → 별도 협의 def _unit_price(rate, lesson, mode): c = rate["coaching"] base = c["lesson_type_prices"][lesson["type"]] if mode == "matrix": lvl = c.get("subject_levels", {}).get(lesson.get("subject")) if lvl: base = base * c["level_multiple"].get(lvl, 1.0) return base def build(scope, rate, catalog): c = rate["coaching"] mode = scope.get("pricing_mode") or c.get("pricing_mode", "base") if scope.get("lessons"): lessons = scope["lessons"] title = scope.get("course") or "맞춤 코칭" else: cname = scope.get("course") courses = catalog.get("courses", {}) if cname not in courses: raise SystemExit(f"course '{cname}' not in catalog {list(courses)}") lessons = courses[cname]["lessons"] title = courses[cname].get("title", cname) items, subtotal = [], 0.0 for L in lessons: up = _unit_price(rate, L, mode) amt = up * L["hours"] items.append({"subject": L.get("subject", ""), "title": L.get("title", ""), "type": L["type"], "hours": L["hours"], "unit_price": up, "amount": amt}) subtotal += amt students = scope.get("students", 1) apply = scope.get("apply_student_discount", c["student_discount"]["apply_default"]) disc = _discount(rate, students, apply) disc_amt = subtotal * disc total = subtotal - disc_amt return { "kind": "coaching", "service": catalog["service"], "label": title, "pricing_mode": mode, "students": students, "discount_rate": disc, "discount_amount": disc_amt, "lessons": items, "subtotal_sum": subtotal, "proposal": int(round(total)), "stub": bool(catalog.get("_stub", False)), }