Complete Economic Observatory MVP (ADAPTIVE-WP-0002)

Add file-based Bubble, Stripe, and OpenRouter importers; usage attribution,
cost allocation, pricing simulator, credit wallets, and recommendations in the
dashboard API. Document whynot-design UI workflow and archive the finished
workplan with all ten tasks marked done.
This commit is contained in:
2026-06-22 23:23:31 +02:00
parent 04ee6d2421
commit 0a38def5a5
26 changed files with 871 additions and 111 deletions

View File

@@ -21,6 +21,10 @@ def test_dashboard_payload_contains_live_ledger_totals() -> None:
assert payload["cost_floor"]["active_price"] == "8.99"
assert len(payload["value_range"]["segments"]) == 2
assert payload["market_price"]["alternative_count"] == 4
assert payload["membership_analytics"]["active_members"] == 1
assert payload["usage"]["record_count"] == 1
assert len(payload["pricing_simulations"]["scenarios"]) == 3
assert payload["recommendations"]
def test_payload_json_is_valid() -> None:

View File

@@ -0,0 +1,42 @@
from __future__ import annotations
from pathlib import Path
from observatory.importers.bubble import import_membership
from observatory.importers.openrouter import import_usage
from observatory.importers.stripe import import_payments
IMPORTS = Path(__file__).resolve().parent.parent / "data" / "imports"
def test_bubble_import_maps_active_member() -> None:
import json
export = json.loads((IMPORTS / "bubble-export.sample.json").read_text(encoding="utf-8"))
payload = import_membership(export)
assert len(payload["members"]) == 1
assert payload["members"][0]["id"] == "member-tegwick"
assert payload["members"][0]["status"] == "active"
assert payload["members"][0]["source"] == "bubble"
def test_stripe_import_normalises_charge() -> None:
import json
export = json.loads((IMPORTS / "stripe-export.sample.json").read_text(encoding="utf-8"))
payload = import_payments(export)
assert payload["records"][0]["gross_amount"] == "8.99"
assert payload["records"][0]["net_amount"] == "8.55"
assert payload["records"][0]["member_username"] == "tegwick"
def test_openrouter_import_converts_cost_to_eur() -> None:
import json
export = json.loads((IMPORTS / "openrouter-export.sample.json").read_text(encoding="utf-8"))
payload = import_usage(export, fx_usd_eur="0.92")
assert payload["records"][0]["cost_eur"] == "0.06"
assert payload["records"][0]["member_id"] == "member-tegwick"

View File

@@ -0,0 +1,96 @@
from __future__ import annotations
from decimal import Decimal
from pathlib import Path
from observatory.allocation import build_cost_allocation
from observatory.api import build_dashboard_payload
from observatory.credits import build_credit_summary, load_credit_wallets
from observatory.economics import build_snapshot
from observatory.load import (
load_membership,
load_monthly_ledger,
load_payment_records,
load_pricing_models,
load_product,
)
from observatory.membership_analytics import build_membership_analytics
from observatory.recommendations import build_pricing_recommendations
from observatory.simulator import build_pricing_simulations
from observatory.usage import build_usage_summary, load_usage_records
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
def _snapshot(period: str = "2026-06"):
product = load_product(DATA_DIR)
models = load_pricing_models(DATA_DIR)
members = load_membership(DATA_DIR)
payments = load_payment_records(DATA_DIR)
ledger = load_monthly_ledger(DATA_DIR)
return build_snapshot(period, product, models, members, payments, ledger)
def test_membership_analytics_counts_active_member() -> None:
members = load_membership(DATA_DIR)
analytics = build_membership_analytics(members, "2026-06", ["2026-05", "2026-06"])
assert analytics["active_members"] == 1
assert analytics["total_members"] == 1
def test_usage_summary_attributes_member_cost() -> None:
records = load_usage_records(DATA_DIR)
summary = build_usage_summary(records, "2026-06")
assert summary["record_count"] == 1
assert summary["by_member"]["member-tegwick"] == Decimal("0.06")
def test_cost_allocation_includes_ai_variable_cost() -> None:
snapshot = _snapshot()
allocation = build_cost_allocation(snapshot, load_usage_records(DATA_DIR))
assert allocation["variable_ai_eur"] == Decimal("0.06")
assert allocation["cost_floor_eur"] == snapshot.cost_per_member
def test_pricing_simulator_compares_candidate_models() -> None:
snapshot = _snapshot()
models = load_pricing_models(DATA_DIR)
simulations = build_pricing_simulations(snapshot, models, Decimal("0.06"))
assert len(simulations["scenarios"]) == 3
assert simulations["active_scenario_id"] == "flat-899-eur-monthly"
def test_credit_summary_tracks_remaining_allowance() -> None:
wallets = load_credit_wallets(DATA_DIR)
summary = build_credit_summary(wallets, {"member-tegwick": Decimal("0.06")}, "2026-06")
assert summary["wallets"][0]["remaining_eur"] == Decimal("1.94")
def test_recommendations_include_hold_or_action() -> None:
payload = build_dashboard_payload(DATA_DIR, "2026-06")
recs = build_pricing_recommendations(
payload["cost_floor"],
payload["value_range"],
payload["market_price"],
payload["pricing_simulations"],
payload["usage"],
)
assert recs
assert recs[0]["id"] in {"margin-pressure", "usage-pricing-signal", "value-headroom", "hold-course"}
def test_dashboard_payload_includes_mvp_sections() -> None:
payload = build_dashboard_payload(DATA_DIR, "2026-06")
assert "membership_analytics" in payload
assert "usage" in payload
assert "cost_allocation" in payload
assert "pricing_simulations" in payload
assert "credit_wallets" in payload
assert "recommendations" in payload