Files
Andrew Yim c9bdbb57f7 Extract ourdigital-estimate-engine; presales-seo now calls it
New skill 96-ourdigital-estimate-engine: method-aware quoting engine
(effort / coaching / procurement) with universal rate_card + per-service
catalog. Real catalogs: seo (effort), education (coaching); stubs:
digital_ads, digital_branding. Validated to reproduce real quotes —
SEO basic ₩10.5M / treatment ₩25.0M, SHR chain ₩29.5M, L'Escape basic
₩10.5M, GA4/GTM coaching ₩1,570,000, procurement +15%.

Refactor 95-ourdigital-presales-seo: remove rate_card.yaml, sow_templates.yaml,
estimate.py (migrated to engine); add findings_to_scope.py; Stage 5 now maps
findings→scope.json and calls the engine CLI. build_deck/kg_query unchanged;
end-to-end validated on SHR (29.5M) + deck renders engine estimate.json.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 01:54:11 +09:00

62 lines
2.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)),
}