generated from coulomb/repo-seed
Implement Stripe publication layer and close WP-0007
This commit is contained in:
@@ -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"),
|
||||
|
||||
159
projects/coulomb-pricing/observatory/publication.py
Normal file
159
projects/coulomb-pricing/observatory/publication.py
Normal 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,
|
||||
}
|
||||
)
|
||||
52
projects/coulomb-pricing/observatory/publish.py
Normal file
52
projects/coulomb-pricing/observatory/publish.py
Normal 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())
|
||||
Reference in New Issue
Block a user