generated from coulomb/repo-seed
Implement canonical pricing core and close WP-0003
This commit is contained in:
@@ -7,10 +7,45 @@
|
||||
"model_type": "flat_subscription",
|
||||
"lifecycle_phase": "growth",
|
||||
"currency": "EUR",
|
||||
"description": "Current flat membership offer for Coulomb Social.",
|
||||
"access_fee_amount": "8.99",
|
||||
"access_fee_cadence": "monthly",
|
||||
"included_usage": "unlimited_repository_access",
|
||||
"status": "active"
|
||||
"status": "active",
|
||||
"charge_components": [
|
||||
{
|
||||
"id": "membership-access",
|
||||
"kind": "access",
|
||||
"amount": "8.99",
|
||||
"cadence": "monthly",
|
||||
"label": "Standard membership access fee",
|
||||
"billing_treatment": "recurring",
|
||||
"metadata": {
|
||||
"included_usage": "unlimited_repository_access"
|
||||
}
|
||||
}
|
||||
],
|
||||
"commitments": [
|
||||
{
|
||||
"id": "baseline-term",
|
||||
"kind": "contract_duration",
|
||||
"value": "1",
|
||||
"unit": "month",
|
||||
"description": "Baseline self-serve monthly term."
|
||||
}
|
||||
],
|
||||
"tunable_parameters": [],
|
||||
"eligibility": [
|
||||
"coulomb-social-members"
|
||||
],
|
||||
"provider_hints": {
|
||||
"stripe": {
|
||||
"collection_method": "charge_automatically"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"catalog_version": "canonical-v1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "membership-plus-credits",
|
||||
@@ -18,10 +53,71 @@
|
||||
"model_type": "hybrid_subscription_usage",
|
||||
"lifecycle_phase": "exploration",
|
||||
"currency": "EUR",
|
||||
"description": "Candidate model bundling recurring access with a monthly AI allowance.",
|
||||
"access_fee_amount": "8.99",
|
||||
"access_fee_cadence": "monthly",
|
||||
"included_usage": "monthly_ai_credit_allowance",
|
||||
"status": "candidate"
|
||||
"status": "candidate",
|
||||
"charge_components": [
|
||||
{
|
||||
"id": "membership-access",
|
||||
"kind": "access",
|
||||
"amount": "8.99",
|
||||
"cadence": "monthly",
|
||||
"label": "Membership base fee",
|
||||
"billing_treatment": "recurring"
|
||||
},
|
||||
{
|
||||
"id": "ai-credit-allowance",
|
||||
"kind": "usage",
|
||||
"meter": "openrouter_tokens",
|
||||
"unit": "tokens",
|
||||
"included_units": "100000",
|
||||
"label": "Included monthly AI token allowance",
|
||||
"billing_treatment": "included",
|
||||
"metadata": {
|
||||
"included_usage": "monthly_ai_credit_allowance"
|
||||
}
|
||||
}
|
||||
],
|
||||
"commitments": [
|
||||
{
|
||||
"id": "credit-prepay-window",
|
||||
"kind": "prepayment",
|
||||
"value": "1",
|
||||
"unit": "month",
|
||||
"description": "Allowance resets monthly in the observatory prototype."
|
||||
}
|
||||
],
|
||||
"tunable_parameters": [
|
||||
{
|
||||
"key": "included_tokens",
|
||||
"parameter_class": "seller_controlled",
|
||||
"data_type": "integer",
|
||||
"description": "Included OpenRouter token allowance for the monthly bundle.",
|
||||
"default_value": "100000",
|
||||
"min_value": "50000",
|
||||
"max_value": "500000"
|
||||
},
|
||||
{
|
||||
"key": "monthly_allowance_eur",
|
||||
"parameter_class": "calculated",
|
||||
"data_type": "decimal",
|
||||
"description": "Observatory-only euro allowance derived from provider usage cost.",
|
||||
"default_value": "2.00"
|
||||
}
|
||||
],
|
||||
"eligibility": [
|
||||
"coulomb-social-members"
|
||||
],
|
||||
"provider_hints": {
|
||||
"stripe": {
|
||||
"metered_usage_strategy": "future_adapter"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"catalog_version": "canonical-v1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "membership-plus-overage",
|
||||
@@ -29,11 +125,82 @@
|
||||
"model_type": "hybrid_subscription_usage",
|
||||
"lifecycle_phase": "exploration",
|
||||
"currency": "EUR",
|
||||
"description": "Candidate model pairing recurring access with included tokens and metered overage.",
|
||||
"access_fee_amount": "8.99",
|
||||
"access_fee_cadence": "monthly",
|
||||
"included_usage": "monthly_ai_credit_allowance",
|
||||
"overage_meter": "openrouter_tokens",
|
||||
"status": "candidate"
|
||||
"status": "candidate",
|
||||
"charge_components": [
|
||||
{
|
||||
"id": "membership-access",
|
||||
"kind": "access",
|
||||
"amount": "8.99",
|
||||
"cadence": "monthly",
|
||||
"label": "Membership base fee",
|
||||
"billing_treatment": "recurring"
|
||||
},
|
||||
{
|
||||
"id": "ai-overage-usage",
|
||||
"kind": "usage",
|
||||
"meter": "openrouter_tokens",
|
||||
"unit": "tokens",
|
||||
"included_units": "100000",
|
||||
"unit_price": "0.002",
|
||||
"label": "OpenRouter token overage",
|
||||
"billing_treatment": "metered",
|
||||
"metadata": {
|
||||
"included_usage": "monthly_ai_credit_allowance"
|
||||
}
|
||||
}
|
||||
],
|
||||
"commitments": [
|
||||
{
|
||||
"id": "baseline-term",
|
||||
"kind": "contract_duration",
|
||||
"value": "1",
|
||||
"unit": "month",
|
||||
"description": "Baseline monthly term; solver can later trade this against usage economics."
|
||||
}
|
||||
],
|
||||
"tunable_parameters": [
|
||||
{
|
||||
"key": "included_tokens",
|
||||
"parameter_class": "customer_tunable",
|
||||
"data_type": "integer",
|
||||
"description": "Customer-selectable included token allowance within seller-approved bounds.",
|
||||
"default_value": "100000",
|
||||
"min_value": "50000",
|
||||
"max_value": "300000"
|
||||
},
|
||||
{
|
||||
"key": "contract_duration_months",
|
||||
"parameter_class": "customer_tunable",
|
||||
"data_type": "integer",
|
||||
"description": "Longer term can support improved usage pricing in later solver milestones.",
|
||||
"default_value": "1",
|
||||
"min_value": "1",
|
||||
"max_value": "12"
|
||||
},
|
||||
{
|
||||
"key": "overage_unit_price",
|
||||
"parameter_class": "calculated",
|
||||
"data_type": "decimal",
|
||||
"description": "Current observatory overage rate derived from scenario assumptions.",
|
||||
"default_value": "0.002"
|
||||
}
|
||||
],
|
||||
"eligibility": [
|
||||
"coulomb-social-members"
|
||||
],
|
||||
"provider_hints": {
|
||||
"stripe": {
|
||||
"meter_name": "openrouter_tokens"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"catalog_version": "canonical-v1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
12
projects/coulomb-pricing/observatory/_repo_root.py
Normal file
12
projects/coulomb-pricing/observatory/_repo_root.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||
|
||||
|
||||
def ensure_repo_root_on_syspath() -> None:
|
||||
root = str(REPO_ROOT)
|
||||
if root not in sys.path:
|
||||
sys.path.insert(0, root)
|
||||
@@ -4,6 +4,10 @@ import json
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
from ._repo_root import ensure_repo_root_on_syspath
|
||||
ensure_repo_root_on_syspath()
|
||||
|
||||
from adaptive_pricing_core.pricing_models import load_pricing_models as load_canonical_pricing_models
|
||||
from .ledger import build_monthly_ledger
|
||||
from .models import (
|
||||
Budget,
|
||||
@@ -50,20 +54,7 @@ def load_budget(data_dir: Path | None = None) -> Budget:
|
||||
|
||||
|
||||
def load_pricing_models(data_dir: Path | None = None) -> list[PricingModel]:
|
||||
raw = _read_json((data_dir or default_data_dir()) / "pricing-models.json")
|
||||
return [
|
||||
PricingModel(
|
||||
id=item["id"],
|
||||
name=item["name"],
|
||||
model_type=item["model_type"],
|
||||
lifecycle_phase=item["lifecycle_phase"],
|
||||
currency=item["currency"],
|
||||
access_fee_amount=_money(item["access_fee_amount"]),
|
||||
access_fee_cadence=item["access_fee_cadence"],
|
||||
status=item["status"],
|
||||
)
|
||||
for item in raw["models"]
|
||||
]
|
||||
return load_canonical_pricing_models((data_dir or default_data_dir()) / "pricing-models.json")
|
||||
|
||||
|
||||
def load_fx_rates(data_dir: Path | None = None) -> dict[str, Decimal]:
|
||||
@@ -150,4 +141,4 @@ def load_monthly_ledger(data_dir: Path | None = None) -> list[MonthlyPlatformCos
|
||||
|
||||
|
||||
def latest_period(monthly_costs: list[MonthlyPlatformCost]) -> str:
|
||||
return max(item.period for item in monthly_costs)
|
||||
return max(item.period for item in monthly_costs)
|
||||
|
||||
@@ -4,9 +4,20 @@ from dataclasses import dataclass
|
||||
from decimal import Decimal
|
||||
from typing import Literal
|
||||
|
||||
from ._repo_root import ensure_repo_root_on_syspath
|
||||
|
||||
ensure_repo_root_on_syspath()
|
||||
|
||||
from adaptive_pricing_core.pricing_models import ( # noqa: E402
|
||||
ChargeComponent,
|
||||
Commitment,
|
||||
PricingModel,
|
||||
PricingModelStatus,
|
||||
TunableParameter,
|
||||
)
|
||||
|
||||
ExpenseClass = Literal["infrastructure", "payment_processing"]
|
||||
MemberStatus = Literal["active", "churned", "paused"]
|
||||
PricingModelStatus = Literal["active", "candidate", "retired"]
|
||||
LiquidityStatus = Literal["burning", "neutral", "generating"]
|
||||
|
||||
|
||||
@@ -20,18 +31,6 @@ class Product:
|
||||
active_pricing_model_id: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PricingModel:
|
||||
id: str
|
||||
name: str
|
||||
model_type: str
|
||||
lifecycle_phase: str
|
||||
currency: str
|
||||
access_fee_amount: Decimal
|
||||
access_fee_cadence: str
|
||||
status: PricingModelStatus
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExpenseRecord:
|
||||
id: str
|
||||
@@ -118,4 +117,4 @@ class LiquiditySummary:
|
||||
cumulative_net_liquidity: Decimal
|
||||
remaining_budget: Decimal
|
||||
liquidity_status: LiquidityStatus
|
||||
months_tracked: int
|
||||
months_tracked: int
|
||||
|
||||
@@ -3,4 +3,8 @@ from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
REPO_ROOT = ROOT.parent.parent
|
||||
if str(REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
35
projects/coulomb-pricing/tests/test_pricing_model_schema.py
Normal file
35
projects/coulomb-pricing/tests/test_pricing_model_schema.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
from adaptive_pricing_core.pricing_models import validate_pricing_catalog
|
||||
from observatory.load import load_pricing_models
|
||||
|
||||
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
|
||||
|
||||
|
||||
def test_coulomb_pricing_catalog_validates() -> None:
|
||||
models = load_pricing_models(DATA_DIR)
|
||||
assert validate_pricing_catalog(models) == {}
|
||||
|
||||
|
||||
def test_hybrid_model_preserves_usage_component_and_tuning_metadata() -> None:
|
||||
models = load_pricing_models(DATA_DIR)
|
||||
model = next(item for item in models if item.id == "membership-plus-overage")
|
||||
|
||||
usage_component = next(component for component in model.charge_components if component.kind == "usage")
|
||||
|
||||
assert usage_component.meter == "openrouter_tokens"
|
||||
assert usage_component.included_units == Decimal("100000")
|
||||
assert usage_component.unit_price == Decimal("0.002")
|
||||
assert any(parameter.parameter_class == "customer_tunable" for parameter in model.tunable_parameters)
|
||||
|
||||
|
||||
def test_flat_model_still_exposes_access_fee_compatibility_fields() -> None:
|
||||
models = load_pricing_models(DATA_DIR)
|
||||
model = next(item for item in models if item.id == "flat-899-eur-monthly")
|
||||
|
||||
assert model.access_fee_amount == Decimal("8.99")
|
||||
assert model.access_fee_cadence == "monthly"
|
||||
assert len(model.charge_components) == 1
|
||||
Reference in New Issue
Block a user