Files
adaptive-pricing/adaptive_pricing_core/stripe_provider.py

331 lines
12 KiB
Python

from __future__ import annotations
from typing import Any
from .provider_publication import (
ProviderMappedArtifact,
ProviderPublicationPackage,
PublicationBundle,
PublishableCommitment,
PublishableConfiguration,
PublishableMeter,
PublishablePrice,
)
def _lookup_key(*parts: str) -> str:
return "--".join(
part.replace(":", "-").replace("_", "-")
for part in parts
if part
)
def _stripe_interval(value: str | None) -> str | None:
if value is None:
return None
normalized = value.lower()
if normalized in {"monthly", "month"}:
return "month"
if normalized in {"yearly", "annual", "year"}:
return "year"
return normalized
def _stripe_hints(bundle: PublicationBundle) -> dict[str, Any]:
return dict(bundle.provider_hints.get("stripe", {}))
def _product_provider_id(bundle: PublicationBundle) -> str:
hints = _stripe_hints(bundle)
return hints.get("product_lookup_key") or _lookup_key("product", bundle.product.product_id)
def _meter_provider_id(bundle: PublicationBundle, meter: PublishableMeter) -> str:
hints = _stripe_hints(bundle)
if hints.get("meter_name") and len(bundle.meters) == 1:
return str(hints["meter_name"])
return _lookup_key("meter", bundle.model_id, meter.meter_id)
def _product_artifact(bundle: PublicationBundle) -> ProviderMappedArtifact:
hints = _stripe_hints(bundle)
return ProviderMappedArtifact(
provider="stripe",
source_key=bundle.product.key,
source_kind="product",
provider_id=_product_provider_id(bundle),
provider_object_type="product",
mapping_status="exact",
payload={
"lookup_key": _product_provider_id(bundle),
"name": bundle.product.name,
"description": bundle.product.description,
"active": bundle.product.active,
"metadata": {
**bundle.product.metadata,
"collection_method": hints.get("collection_method", "charge_automatically"),
"source_of_truth": "adaptive-pricing",
},
},
metadata={"model_id": bundle.model_id},
notes=("Stripe product mapping is direct for catalog identity and metadata.",),
)
def _meter_artifact(bundle: PublicationBundle, meter: PublishableMeter) -> ProviderMappedArtifact:
provider_id = _meter_provider_id(bundle, meter)
return ProviderMappedArtifact(
provider="stripe",
source_key=meter.key,
source_kind="meter",
provider_id=provider_id,
provider_object_type="billing_meter",
mapping_status="exact",
payload={
"lookup_key": provider_id,
"display_name": meter.name,
"event_name": meter.event_name,
"default_aggregation": {"formula": meter.aggregation},
"unit_label": meter.unit,
"metadata": {
**meter.metadata,
"source_of_truth": "adaptive-pricing",
},
},
metadata={"model_id": bundle.model_id},
notes=("Stripe meter mapping is direct for metered usage identifiers.",),
)
def _fixed_price_payload(bundle: PublicationBundle, price: PublishablePrice) -> dict[str, Any]:
payload = {
"lookup_key": _lookup_key("price", bundle.model_id, price.component_id),
"product": _product_provider_id(bundle),
"currency": price.currency.lower(),
"nickname": price.label,
"unit_amount_decimal": str(price.amount or "0"),
"metadata": {
**price.metadata,
"source_of_truth": "adaptive-pricing",
},
}
if price.billing_treatment != "one_time" and price.cadence:
payload["recurring"] = {"interval": _stripe_interval(price.cadence)}
return payload
def _discount_artifact(bundle: PublicationBundle, price: PublishablePrice) -> ProviderMappedArtifact:
provider_id = _lookup_key("coupon", bundle.model_id, price.component_id)
return ProviderMappedArtifact(
provider="stripe",
source_key=price.key,
source_kind="price",
provider_id=provider_id,
provider_object_type="coupon",
mapping_status="approximate",
payload={
"lookup_key": provider_id,
"amount_off_decimal": str(abs(price.amount or 0)),
"currency": price.currency.lower(),
"name": price.label,
"metadata": {
**price.metadata,
"source_of_truth": "adaptive-pricing",
},
},
metadata={"model_id": bundle.model_id},
notes=(
"Stripe coupons approximate discount components because attachment to subscriptions and eligibility rules lives outside the price object.",
),
)
def _usage_price_artifact(
bundle: PublicationBundle,
price: PublishablePrice,
) -> ProviderMappedArtifact:
provider_id = _lookup_key("price", bundle.model_id, price.component_id)
if price.meter_key is None or price.unit_price is None:
return ProviderMappedArtifact(
provider="stripe",
source_key=price.key,
source_kind="price",
provider_id=provider_id,
provider_object_type="price",
mapping_status="unsupported",
payload={
"lookup_key": provider_id,
"reason": "usage component lacks a billable per-unit price or mapped meter",
},
metadata={"model_id": bundle.model_id},
notes=(
"Stripe publication cannot create a metered price without both a meter and a per-unit charge.",
),
)
meter_provider_id = _lookup_key("meter", bundle.model_id, price.component_id)
if len(bundle.meters) == 1:
meter_provider_id = _meter_provider_id(bundle, bundle.meters[0])
status = "approximate" if price.included_units not in (None, 0) else "exact"
notes = [
"Stripe metered price mapping is direct for per-unit overage billing.",
]
if status == "approximate":
notes.append(
"Included usage allowance requires supplemental credits, invoice adjustments, or custom entitlement logic outside the Stripe price object."
)
return ProviderMappedArtifact(
provider="stripe",
source_key=price.key,
source_kind="price",
provider_id=provider_id,
provider_object_type="price",
mapping_status=status,
payload={
"lookup_key": provider_id,
"product": _product_provider_id(bundle),
"currency": price.currency.lower(),
"nickname": price.label,
"billing_scheme": "per_unit",
"unit_amount_decimal": str(price.unit_price),
"recurring": {
"interval": _stripe_interval(price.cadence) or "month",
"usage_type": "metered",
},
"meter": meter_provider_id,
"metadata": {
**price.metadata,
"included_units": str(price.included_units) if price.included_units is not None else None,
"source_of_truth": "adaptive-pricing",
},
},
metadata={"model_id": bundle.model_id},
notes=tuple(notes),
)
def _price_artifact(bundle: PublicationBundle, price: PublishablePrice) -> ProviderMappedArtifact:
provider_id = _lookup_key("price", bundle.model_id, price.component_id)
if price.component_kind == "usage":
return _usage_price_artifact(bundle, price)
if price.component_kind == "discount":
return _discount_artifact(bundle, price)
return ProviderMappedArtifact(
provider="stripe",
source_key=price.key,
source_kind="price",
provider_id=provider_id,
provider_object_type="price",
mapping_status="exact",
payload=_fixed_price_payload(bundle, price),
metadata={"model_id": bundle.model_id},
notes=("Stripe price mapping is direct for fixed recurring or one-time charges.",),
)
def _commitment_artifact(
bundle: PublicationBundle,
commitment: PublishableCommitment,
) -> ProviderMappedArtifact:
provider_id = _lookup_key("commitment", bundle.model_id, commitment.commitment_id)
if commitment.kind == "contract_duration":
return ProviderMappedArtifact(
provider="stripe",
source_key=commitment.key,
source_kind="commitment",
provider_id=provider_id,
provider_object_type="metadata_binding",
mapping_status="approximate",
payload={
"lookup_key": provider_id,
"metadata": {
**commitment.metadata,
"contract_duration_value": commitment.value,
"contract_duration_unit": commitment.unit,
},
},
metadata={"model_id": bundle.model_id},
notes=(
"Stripe can store contract duration metadata, but enforcement still relies on subscription schedule policy or external contract workflow.",
),
)
return ProviderMappedArtifact(
provider="stripe",
source_key=commitment.key,
source_kind="commitment",
provider_id=provider_id,
provider_object_type="metadata_binding",
mapping_status="unsupported",
payload={
"lookup_key": provider_id,
"kind": commitment.kind,
"value": commitment.value,
"unit": commitment.unit,
},
metadata={"model_id": bundle.model_id},
notes=(
"Stripe metadata alone cannot enforce this commitment semantics; it remains an internal pricing and contract artifact.",
),
)
def _configuration_artifact(
bundle: PublicationBundle,
configuration: PublishableConfiguration,
) -> ProviderMappedArtifact:
provider_id = _lookup_key("configuration", configuration.configuration_id)
return ProviderMappedArtifact(
provider="stripe",
source_key=configuration.key,
source_kind="configuration",
provider_id=provider_id,
provider_object_type="metadata_binding",
mapping_status="approximate",
payload={
"lookup_key": provider_id,
"metadata": {
**configuration.metadata,
"configuration_id": configuration.configuration_id,
"segment": configuration.segment,
"price_keys": list(configuration.price_keys),
"commitment_keys": list(configuration.commitment_keys),
},
},
metadata={"model_id": bundle.model_id},
notes=(
"Customer or default pricing configurations can be recorded in Stripe metadata, but they are not first-class Stripe catalog objects.",
),
)
def map_bundle_to_stripe(bundle: PublicationBundle) -> ProviderPublicationPackage:
artifacts: list[ProviderMappedArtifact] = [_product_artifact(bundle)]
artifacts.extend(_meter_artifact(bundle, meter) for meter in bundle.meters)
artifacts.extend(_price_artifact(bundle, price) for price in bundle.prices)
artifacts.extend(_commitment_artifact(bundle, commitment) for commitment in bundle.commitments)
artifacts.extend(
_configuration_artifact(bundle, configuration)
for configuration in bundle.configurations
)
notes = [
"Stripe remains an execution backend; adaptive-pricing stays the source of truth.",
"Exact mappings create publishable Stripe shadow artifacts; approximate mappings require supplemental operational logic.",
]
if _stripe_hints(bundle).get("metered_usage_strategy") == "future_adapter":
notes.append(
"The pricing model declares a future metered-usage strategy hint, so Stripe publication keeps the usage allowance semantics descriptive rather than executable."
)
return ProviderPublicationPackage(
provider="stripe",
bundle_id=bundle.bundle_id,
model_id=bundle.model_id,
model_name=bundle.model_name,
artifacts=tuple(artifacts),
notes=tuple(notes),
)