diff --git a/pricing.py b/pricing.py new file mode 100644 index 0000000..60afd5f --- /dev/null +++ b/pricing.py @@ -0,0 +1,109 @@ +"""ConnectVM rate card (INR) and pricing engine. + +Model: flavor-based monthly pricing, prorated by billable hours in the +billing month. A VM present for the whole month pays its full flavor price; +created mid-month it prorates. Custom flavors (not on the card) are priced +from add-on unit rates. Volumes and floating IPs use add-on rates. +""" + +# ---- Rate card: flavor monthly prices (INR) ------------------------------- +# key = normalized flavor name ("B1 - Lite" / "B1 · Lite" -> "b1 lite") +FLAVOR_MONTHLY = { + "m2 tiny": 1200.0, + "m1 standard": 2303.0, + "a1 nano": 2303.0, + "b1 lite": 4607.0, + "c1 core": 9215.0, + "r1 medium": 27647.0, + "s1 large": 34559.0, + "t3 max": 43199.0, + "d2 edge": 46079.0, + "t2 xlarge": 62207.0, + "w2 prime": 73727.0, + "x1 elite": 92159.0, +} + +# Card specs (vcpus, ram_mb, disk_gb) to detect spec drift on named flavors +FLAVOR_SPECS = { + "m2 tiny": (1, 2048, 10), + "m1 standard": (1, 4096, 20), + "a1 nano": (2, 4096, 20), + "b1 lite": (4, 8192, 40), + "c1 core": (8, 16384, 80), + "r1 medium": (16, 65536, 200), + "s1 large": (24, 65536, 300), + "t3 max": (32, 131072, 480), + "d2 edge": (12, 32768, 100), + "t2 xlarge": (32, 98304, 480), + "w2 prime": (42, 131072, 500), + "x1 elite": (64, 131072, 800), +} + +# ---- Add-on rates (INR/month) --------------------------------------------- +ADDON_VCPU = 480.0 # per vCPU +ADDON_RAM_PER_GB = 192.0 # per GB RAM +ADDON_DISK_PER_GB = 9.6 # ₹96 per 10 GB SSD +FIP_MONTHLY = 300.0 # additional IPv4 +SNAPSHOT_PER_10GB = 48.0 # not metered yet +LB_MONTHLY = 1440.0 # not metered yet (Octavia) +BACKUP_PER_10GB = 72.0 # not metered yet +BANDWIDTH_PER_GB = 1.80 # not metered yet + +RATE_CARD_INFO = { + "currency": "INR", + "model": "flavor monthly price prorated by billable hours; " + "custom flavors computed from add-on rates", + "addon_rates_per_month": { + "vcpu": ADDON_VCPU, + "ram_gb": ADDON_RAM_PER_GB, + "ssd_gb": ADDON_DISK_PER_GB, + "floating_ip": FIP_MONTHLY, + }, + "not_yet_metered": { + "snapshot_per_10gb": SNAPSHOT_PER_10GB, + "load_balancer": LB_MONTHLY, + "backup_per_10gb": BACKUP_PER_10GB, + "bandwidth_per_gb": BANDWIDTH_PER_GB, + }, +} + + +def normalize_flavor(name: str) -> str: + for ch in ("·", "-", "_", "."): + name = name.replace(ch, " ") + return " ".join(name.lower().split()) + + +def flavor_monthly(name: str, vcpus: int, ram_mb: int, disk_gb: int): + """Return (monthly_price_inr, source, spec_mismatch).""" + key = normalize_flavor(name or "") + if key in FLAVOR_MONTHLY: + spec = FLAVOR_SPECS[key] + mismatch = spec != (vcpus, ram_mb, disk_gb) + return FLAVOR_MONTHLY[key], "rate-card", mismatch + price = ( + vcpus * ADDON_VCPU + + (ram_mb / 1024) * ADDON_RAM_PER_GB + + disk_gb * ADDON_DISK_PER_GB + ) + return price, "computed", False + + +def period_hours(begin, end) -> float: + return (end - begin).total_seconds() / 3600 + + +def inr(amount: float) -> str: + """Format with Indian digit grouping: 92,159.00 / 1,23,456.00""" + neg = amount < 0 + whole, _, frac = f"{abs(amount):.2f}".partition(".") + if len(whole) > 3: + head, tail = whole[:-3], whole[-3:] + groups = [] + while len(head) > 2: + groups.insert(0, head[-2:]) + head = head[:-2] + if head: + groups.insert(0, head) + whole = ",".join(groups + [tail]) + return f"₹{'-' if neg else ''}{whole}.{frac}"