From 95d6fdf499410d61de703c0f394c945ecc92ff9b Mon Sep 17 00:00:00 2001 From: Andrew Yim Date: Thu, 28 May 2026 01:15:55 +0900 Subject: [PATCH] Add luxury/vertical signal to auto-tiering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pick_baseline now applies a premium floor: if prospect.vertical matches rate_card.tiering.premium_verticals (luxury/premium/deluxe/5성/특1급…), the auto-selected tier is floored to premium_min_tier (default basic) so a premium single property won't drop to the smb entry tier. Validated: L'Escape (hotel_luxury, 1 property) smb -> basic (₩10.5M); non-luxury single -> smb (₩3.0M); SHR chain -> treatment (₩29.5M) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../95-ourdigital-presales-seo/SKILL.md | 2 +- .../references/findings_to_service.md | 6 ++-- .../references/rate_card.yaml | 7 +++++ .../scripts/estimate.py | 30 +++++++++++++++---- 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/custom-skills/95-ourdigital-presales-seo/SKILL.md b/custom-skills/95-ourdigital-presales-seo/SKILL.md index d820e34..b77aa86 100644 --- a/custom-skills/95-ourdigital-presales-seo/SKILL.md +++ b/custom-skills/95-ourdigital-presales-seo/SKILL.md @@ -56,7 +56,7 @@ headless Chrome, python-pptx. Create `data/` subfolder. Initialize `findings.jso ## Stage 5 — Estimate (견적) — REVIEW GATE - `python scripts/estimate.py --findings /data/findings.json --rate-card references/rate_card.yaml --sow references/sow_templates.yaml --out-dir --seq [--baseline basic|treatment] [--billing 0.70]` -- **Effort-based** (OurDigital real model): cost = role_rate × 청구율 × 표준 업무시간, by module; 제안가 = 합계 절사. findings auto-select a tier (**smb / basic / treatment**) and scale **On-page** hours sub-linearly by `subbrands_total` (cap ×2.0); Technical is fixed. `smb` tier bills at 0.55, others 0.70. Override with `--baseline` / `--billing` (quote premium single properties as basic/treatment manually). +- **Effort-based** (OurDigital real model): cost = role_rate × 청구율 × 표준 업무시간, by module; 제안가 = 합계 절사. findings auto-select a tier (**smb / basic / treatment**) and scale **On-page** hours sub-linearly by `subbrands_total` (cap ×2.0); Technical is fixed. `smb` tier bills at 0.55, others 0.70. Override with `--baseline` / `--billing`. Premium verticals (luxury/5성/특1급…) are auto-floored to `basic` via `rate_card.tiering` (so a luxury single property won't drop to `smb`). - Produces `05_estimate_ko.md`, `05_estimate.xlsx`, `data/estimate.json`. Present the 견적; get sign-off. - Logic in `references/findings_to_service.md`; rates/hours in `rate_card.yaml` + `sow_templates.yaml` (edit together). Reproduces real Basic ₩10.5M / Treatment ₩25.0M; SMB entry ~₩3M; chains stay SMB-acceptable (e.g. 25-property → ~₩29.5M). diff --git a/custom-skills/95-ourdigital-presales-seo/references/findings_to_service.md b/custom-skills/95-ourdigital-presales-seo/references/findings_to_service.md index 465aa8a..598d686 100644 --- a/custom-skills/95-ourdigital-presales-seo/references/findings_to_service.md +++ b/custom-skills/95-ourdigital-presales-seo/references/findings_to_service.md @@ -8,8 +8,10 @@ floored to `rounding_unit`. Mirrors OurDigital/D.intelligence's real SOW-based q - `treatment` if `properties_total > 5` OR `subbrands_total > 3` (multi-brand / chain) - `smb` if `properties_total <= 1` AND `subbrands_total == 0` (single-property SMB) - `basic` otherwise (small multi-property / mid) -- override with `--baseline`. **Note:** tiering is by portfolio size only — quote a - single *premium* property as `basic`/`treatment` manually. +- **Premium floor:** if `prospect.vertical` matches `rate_card.tiering.premium_verticals` + (luxury/premium/deluxe/5성/특1급…), the auto-tier is floored to `tiering.premium_min_tier` + (default `basic`) — a luxury single property won't auto-drop to `smb`. +- override with `--baseline`. Per-tier billing: `smb` uses `billing_rate: 0.55` (set in sow_templates); `basic`/ `treatment` use `rate_card.billing_rate` (0.70). Override with `--billing`. diff --git a/custom-skills/95-ourdigital-presales-seo/references/rate_card.yaml b/custom-skills/95-ourdigital-presales-seo/references/rate_card.yaml index a93cdb0..9b79e44 100644 --- a/custom-skills/95-ourdigital-presales-seo/references/rate_card.yaml +++ b/custom-skills/95-ourdigital-presales-seo/references/rate_card.yaml @@ -27,6 +27,13 @@ terms: validity_days: 14 # 견적 유효기간(약 2주) payment: "현금" +# 자동 티어링 보정. 프리미엄/럭셔리 prospect 는 규모가 작아도 entry(smb) 아래로 +# 자동 선택되지 않도록 최소 티어를 보장. prospect.vertical 문자열에 아래 용어가 +# 포함되면 프리미엄으로 간주(부분 일치, 대소문자 무시). +tiering: + premium_verticals: ["luxury", "premium", "deluxe", "5_star", "5성", "특1급", "boutique_luxury"] + premium_min_tier: basic # 프리미엄 단일 프로퍼티 최소 티어 (smb < basic < treatment) + # 시간 단가 (KRW/hr) — 직급 기준. sow_templates.yaml의 task.role 이 이 키를 참조. role_rates: ceo: 180000 # 대표 diff --git a/custom-skills/95-ourdigital-presales-seo/scripts/estimate.py b/custom-skills/95-ourdigital-presales-seo/scripts/estimate.py index f5271d7..3b795db 100755 --- a/custom-skills/95-ourdigital-presales-seo/scripts/estimate.py +++ b/custom-skills/95-ourdigital-presales-seo/scripts/estimate.py @@ -44,17 +44,35 @@ def scope_multiplier(rate, f): return float(bands[-1][1]), driver, count -def pick_baseline(f, override): +TIER_ORDER = {"smb": 0, "basic": 1, "treatment": 2} + + +def _higher(a, b): + return a if TIER_ORDER.get(a, 0) >= TIER_ORDER.get(b, 0) else b + + +def is_premium(f, rate): + vertical = (f.get("prospect", {}).get("vertical") or "").lower() + terms = [t.lower() for t in rate.get("tiering", {}).get("premium_verticals", [])] + return any(t in vertical for t in terms) + + +def pick_baseline(f, override, rate): if override: return override e = f.get("entity", {}) props = e.get("properties_total", 0) or 0 subs = e.get("subbrands_total", 0) or 0 if props > 5 or subs > 3: # multi-brand / chain - return "treatment" - if props <= 1 and subs == 0: # single-property SMB - return "smb" - return "basic" # small multi-property / mid + tier = "treatment" + elif props <= 1 and subs == 0: # single property + tier = "smb" + else: # small multi-property / mid + tier = "basic" + # premium/luxury floor: don't auto-drop a premium prospect to the entry tier + if is_premium(f, rate): + tier = _higher(tier, rate.get("tiering", {}).get("premium_min_tier", "basic")) + return tier def assemble(f, rate, sow, baseline, billing): @@ -176,7 +194,7 @@ def main(): with open(args.sow, encoding="utf-8") as fh: sow = yaml.safe_load(fh) - baseline = pick_baseline(f, args.baseline) + baseline = pick_baseline(f, args.baseline, rate) tpl_billing = sow["baselines"][baseline].get("billing_rate") billing = (args.billing if args.billing is not None else tpl_billing if tpl_billing is not None else rate["billing_rate"])