Implement Stripe publication layer and close WP-0007

This commit is contained in:
codex
2026-07-03 01:08:29 +02:00
parent 124ad48720
commit a76e57ba89
11 changed files with 1508 additions and 9 deletions

View File

@@ -60,4 +60,17 @@ python3 -m observatory.importers.openrouter --input data/imports/openrouter-expo
```
Sample exports live under `data/imports/`. Live API sync can replace these
file-based importers in a follow-on workplan.
file-based importers in a follow-on workplan.
### Stripe Publication (shadow state)
```bash
cd projects/coulomb-pricing
python3 -m observatory.publish --model-id flat-899-eur-monthly
python3 -m observatory.publish --model-id flat-899-eur-monthly --apply
python3 -m observatory.publish --rollback stripe-rev-0001
```
These commands preview, apply, and roll back the local Stripe shadow state used
by the provider-publication MVP. The live Stripe API is still outside this
milestone.

View File

@@ -26,6 +26,7 @@ from .boundary import build_boundary_validation
from .credits import build_credit_summary, load_credit_wallets
from .membership_analytics import build_membership_analytics
from .pricing_context import build_cost_floor, build_market_price_view, build_value_range_view
from .publication import build_stripe_publication_preview
from .recommendations import build_pricing_recommendations
from .simulator import build_pricing_simulations
from .tuning import build_customer_tuning_pilot
@@ -122,6 +123,12 @@ def build_dashboard_payload(data_dir: Path | None = None, period: str | None = N
recommendations = build_pricing_recommendations(
cost_floor, value_range, market_price, simulations, usage_summary
)
provider_publication = build_stripe_publication_preview(
product,
models,
root,
model_id=product.active_pricing_model_id,
)
return _serialize(
{
@@ -149,6 +156,7 @@ def build_dashboard_payload(data_dir: Path | None = None, period: str | None = N
"boundary_validation": boundary_validation,
"credit_wallets": credit_summary,
"recommendations": recommendations,
"provider_publication": provider_publication,
"infrastructure": {
"domains": _load_json_catalog(root, "domains.json"),
"virtual_servers": _load_json_catalog(root, "virtual_servers.json"),

View File

@@ -0,0 +1,159 @@
from __future__ import annotations
import json
from decimal import Decimal
from pathlib import Path
from typing import Any
from ._repo_root import ensure_repo_root_on_syspath
from .models import PricingModel, Product
ensure_repo_root_on_syspath()
from adaptive_pricing_core.provider_publication import ( # noqa: E402
CatalogProduct,
ProviderPublicationState,
apply_publication,
build_publication_bundle,
plan_publication,
provider_state_from_dict,
provider_state_to_dict,
rollback_publication,
)
from adaptive_pricing_core.stripe_provider import map_bundle_to_stripe # noqa: E402
def _serialize(value: Any) -> Any:
if isinstance(value, Decimal):
return str(value)
if hasattr(value, "__dataclass_fields__"):
return {key: _serialize(getattr(value, key)) for key in value.__dataclass_fields__}
if isinstance(value, tuple):
return [_serialize(item) for item in value]
if isinstance(value, list):
return [_serialize(item) for item in value]
if isinstance(value, dict):
return {key: _serialize(item) for key, item in value.items()}
return value
def default_stripe_state_path(data_dir: Path) -> Path:
return data_dir / "provider_state" / "stripe-publication.json"
def load_stripe_publication_state(path: Path) -> ProviderPublicationState:
if not path.exists():
return ProviderPublicationState(provider="stripe")
return provider_state_from_dict(json.loads(path.read_text(encoding="utf-8")))
def save_stripe_publication_state(path: Path, state: ProviderPublicationState) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(provider_state_to_dict(state), indent=2),
encoding="utf-8",
)
def _catalog_product(product: Product) -> CatalogProduct:
return CatalogProduct(
id=product.id,
name=product.name,
description=product.description,
currency=product.currency,
lifecycle_phase=product.lifecycle_phase,
active_pricing_model_id=product.active_pricing_model_id,
metadata={"product_channel": "membership"},
)
def _target_model(
models: list[PricingModel],
product: Product,
model_id: str | None = None,
) -> PricingModel:
requested_id = model_id or product.active_pricing_model_id
return next(item for item in models if item.id == requested_id)
def build_stripe_publication_preview(
product: Product,
models: list[PricingModel],
data_dir: Path,
*,
model_id: str | None = None,
state_path: Path | None = None,
) -> dict[str, Any]:
model = _target_model(models, product, model_id)
bundle = build_publication_bundle(_catalog_product(product), model)
package = map_bundle_to_stripe(bundle)
state = load_stripe_publication_state(state_path or default_stripe_state_path(data_dir))
plan = plan_publication(package, state)
return _serialize(
{
"provider": "stripe",
"state_path": str(state_path or default_stripe_state_path(data_dir)),
"model_id": model.id,
"model_name": model.name,
"current_state": {
"active_revision_id": state.active_revision_id,
"active_model_id": state.active_model_id,
"artifact_count": len(state.artifacts),
"revision_count": len(state.revisions),
},
"artifact_counts": {
"exact": sum(item.mapping_status == "exact" for item in package.artifacts),
"approximate": sum(item.mapping_status == "approximate" for item in package.artifacts),
"unsupported": sum(item.mapping_status == "unsupported" for item in package.artifacts),
},
"plan": plan,
"notes": package.notes,
}
)
def publish_to_stripe_shadow(
product: Product,
models: list[PricingModel],
data_dir: Path,
*,
model_id: str | None = None,
state_path: Path | None = None,
) -> dict[str, Any]:
path = state_path or default_stripe_state_path(data_dir)
model = _target_model(models, product, model_id)
bundle = build_publication_bundle(_catalog_product(product), model)
package = map_bundle_to_stripe(bundle)
current_state = load_stripe_publication_state(path)
result = apply_publication(package, current_state)
save_stripe_publication_state(path, result.state)
return _serialize(
{
"provider": "stripe",
"state_path": str(path),
"model_id": model.id,
"model_name": model.name,
"result": result,
}
)
def rollback_stripe_shadow(
data_dir: Path,
revision_id: str,
*,
state_path: Path | None = None,
) -> dict[str, Any]:
path = state_path or default_stripe_state_path(data_dir)
current_state = load_stripe_publication_state(path)
result = rollback_publication(current_state, revision_id)
save_stripe_publication_state(path, result.state)
return _serialize(
{
"provider": "stripe",
"state_path": str(path),
"result": result,
}
)

View File

@@ -0,0 +1,52 @@
from __future__ import annotations
import argparse
import json
from pathlib import Path
from .load import default_data_dir, load_pricing_models, load_product
from .publication import (
build_stripe_publication_preview,
publish_to_stripe_shadow,
rollback_stripe_shadow,
)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Preview or apply Stripe publication for pricing models")
parser.add_argument("--data-dir", type=Path, default=default_data_dir())
parser.add_argument("--model-id", help="Pricing model id to preview or publish")
parser.add_argument("--provider", default="stripe", choices=["stripe"])
parser.add_argument("--state-path", type=Path, help="Override provider shadow-state path")
parser.add_argument("--apply", action="store_true", help="Apply the publication plan to the local Stripe shadow state")
parser.add_argument("--rollback", help="Rollback the local Stripe shadow state to a prior revision id")
args = parser.parse_args(argv)
product = load_product(args.data_dir)
models = load_pricing_models(args.data_dir)
if args.rollback:
payload = rollback_stripe_shadow(args.data_dir, args.rollback, state_path=args.state_path)
elif args.apply:
payload = publish_to_stripe_shadow(
product,
models,
args.data_dir,
model_id=args.model_id,
state_path=args.state_path,
)
else:
payload = build_stripe_publication_preview(
product,
models,
args.data_dir,
model_id=args.model_id,
state_path=args.state_path,
)
print(json.dumps(payload, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -30,6 +30,9 @@ def test_dashboard_payload_contains_live_ledger_totals() -> None:
assert payload["pricing_simulations"]["reference_model_id"] is not None
assert payload["customer_tuning"]["request_count"] == 2
assert payload["customer_tuning"]["accepted_request_ids"] == ["small-team-lower-usage-price"]
assert payload["provider_publication"]["provider"] == "stripe"
assert payload["provider_publication"]["model_id"] == "flat-899-eur-monthly"
assert payload["provider_publication"]["plan"]["summary"].startswith("stripe:")
assert len(payload["boundary_validation"]["model_results"]) == 3
assert payload["boundary_validation"]["policy"]["target_margin_pct"] == "15"
assert any(

View File

@@ -0,0 +1,124 @@
from __future__ import annotations
import json
from pathlib import Path
from observatory.load import load_pricing_models, load_product
from observatory.publication import (
build_stripe_publication_preview,
publish_to_stripe_shadow,
rollback_stripe_shadow,
)
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
def _catalog():
return load_product(DATA_DIR), load_pricing_models(DATA_DIR)
def test_overage_preview_includes_meter_and_approximate_mapping(tmp_path: Path) -> None:
product, models = _catalog()
preview = build_stripe_publication_preview(
product,
models,
DATA_DIR,
model_id="membership-plus-overage",
state_path=tmp_path / "stripe-state.json",
)
assert preview["artifact_counts"]["exact"] >= 3
assert preview["artifact_counts"]["approximate"] >= 1
assert preview["artifact_counts"]["unsupported"] == 0
assert any(
operation["provider_object_type"] == "billing_meter"
for operation in preview["plan"]["operations"]
)
def test_credit_allowance_preview_marks_unsupported_usage_mapping(tmp_path: Path) -> None:
product, models = _catalog()
preview = build_stripe_publication_preview(
product,
models,
DATA_DIR,
model_id="membership-plus-credits",
state_path=tmp_path / "stripe-state.json",
)
unsupported = {
artifact["source_key"]
for artifact in preview["plan"]["unsupported_artifacts"]
}
assert "price:membership-plus-credits:ai-credit-allowance" in unsupported
assert preview["artifact_counts"]["unsupported"] >= 1
def test_publication_is_idempotent_and_detects_drift(tmp_path: Path) -> None:
product, models = _catalog()
state_path = tmp_path / "stripe-state.json"
publish_to_stripe_shadow(
product,
models,
DATA_DIR,
model_id="flat-899-eur-monthly",
state_path=state_path,
)
preview = build_stripe_publication_preview(
product,
models,
DATA_DIR,
model_id="flat-899-eur-monthly",
state_path=state_path,
)
assert {operation["kind"] for operation in preview["plan"]["operations"]} == {"noop"}
raw_state = json.loads(state_path.read_text(encoding="utf-8"))
price_artifact = next(
artifact
for artifact in raw_state["artifacts"]
if artifact["provider_id"] == "price--flat-899-eur-monthly--membership-access"
)
price_artifact["payload"]["unit_amount_decimal"] = "9.49"
state_path.write_text(json.dumps(raw_state, indent=2), encoding="utf-8")
drifted = build_stripe_publication_preview(
product,
models,
DATA_DIR,
model_id="flat-899-eur-monthly",
state_path=state_path,
)
assert drifted["plan"]["drift"]
assert any(operation["kind"] == "update" for operation in drifted["plan"]["operations"])
def test_publication_rollback_restores_prior_revision(tmp_path: Path) -> None:
product, models = _catalog()
state_path = tmp_path / "stripe-state.json"
first = publish_to_stripe_shadow(
product,
models,
DATA_DIR,
model_id="flat-899-eur-monthly",
state_path=state_path,
)
publish_to_stripe_shadow(
product,
models,
DATA_DIR,
model_id="membership-plus-overage",
state_path=state_path,
)
rollback = rollback_stripe_shadow(
DATA_DIR,
first["result"]["revision"]["revision_id"],
state_path=state_path,
)
assert rollback["result"]["state"]["active_model_id"] == "flat-899-eur-monthly"
assert rollback["result"]["revision"]["summary"].startswith("Rolled back")