Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion agentscore_commerce/checkout.py
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,31 @@ def _mpp_rail_key(self) -> str:
return key
return "tempo"

def _rails_key_for_mppx_method(self, method: str) -> str | None:
"""Map an mppx credential ``method`` to the merchant's rails-dict key.

Used in ``_handle_mppx`` so the settle outcome distinguishes Solana
from Tempo (both fall under ``rail="mpp"``) and from Stripe SPT.
``method`` is one of ``tempo`` / ``solana`` / ``stripe``. Returns
``None`` when the merchant has no rail registered for that method.
"""
if method == "stripe":
for key, spec in self.rails.items():
if isinstance(spec, StripeRailSpec):
return key
return None
if method == "solana":
for key, spec in self.rails.items():
if isinstance(spec, SolanaMppRailSpec):
return key
return None
if method == "tempo":
for key, spec in self.rails.items():
if isinstance(spec, (TempoRailSpec, TempoSessionRailSpec)):
return key
return None
return None

@property
def _x402_base_network(self) -> str | None:
"""CAIP-2 read from ``rails['x402_base'].network`` (or its default).
Expand Down Expand Up @@ -1761,9 +1786,20 @@ async def _handle_mppx(self, ctx: CheckoutContext) -> CheckoutResult:
raise RuntimeError(msg)
composed: MppxComposeOutcome = await _maybe_await(self.compose_mppx(ctx))
if composed.status == 200:
receipt_method: str | None = None
raw_receipt = composed.raw.get("receipt") if isinstance(composed.raw, dict) else None
if isinstance(raw_receipt, dict):
m = raw_receipt.get("method")
if isinstance(m, str):
receipt_method = m
elif raw_receipt is not None:
m = getattr(raw_receipt, "method", None)
if isinstance(m, str):
receipt_method = m
derived_key = self._rails_key_for_mppx_method(receipt_method) if receipt_method is not None else None
outcome = SettleOutcome(
rail="mpp",
rail_key=composed.rail_key,
rail_key=derived_key or composed.rail_key or self._mpp_rail_key(),
tx_hash=composed.tx_hash,
signer_address=composed.signer_address,
signer_network=composed.signer_network,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "agentscore-commerce"
version = "2.0.1"
version = "2.0.2"
description = "Agent commerce SDK for Python — identity middleware (FastAPI, Flask, Django, AIOHTTP, Sanic, ASGI) + payment helpers + 402 builders + discovery + Stripe multichain. The full merchant-side toolkit for AgentScore-powered agent commerce."
readme = "README.md"
license = "MIT"
Expand Down
36 changes: 36 additions & 0 deletions tests/test_checkout.py
Original file line number Diff line number Diff line change
Expand Up @@ -573,3 +573,39 @@ def test_init_requires_x402_base_railspec_when_x402_server_provided() -> None:
compute_pricing=lambda _ctx: PricingResult(amount_usd=1.0),
x402_server=object(),
)


def test_rails_key_for_mppx_method_picks_the_matching_spec() -> None:
"""``_rails_key_for_mppx_method`` maps mppx credential methods (``tempo`` /
``solana`` / ``stripe``) to the merchant's actual rails-dict key, so MPP
settlements distinguish Solana from Tempo (both fall under
``SettleOutcome.rail="mpp"``) and from Stripe SPT.
"""
checkout = Checkout(
rails={
"tempo_charge": TempoRailSpec(recipient="0xtempo"),
"x402_base": X402BaseRailSpec(recipient="0xbase"),
"sol_rail": SolanaMppRailSpec(recipient="solanaaddr"),
"stripe_spt": StripeRailSpec(profile_id="profile_x"),
},
url="https://api.example/purchase",
compute_pricing=lambda _ctx: PricingResult(amount_usd=1.0),
)
assert checkout._rails_key_for_mppx_method("tempo") == "tempo_charge"
assert checkout._rails_key_for_mppx_method("solana") == "sol_rail"
assert checkout._rails_key_for_mppx_method("stripe") == "stripe_spt"
assert checkout._rails_key_for_mppx_method("unknown") is None


def test_rails_key_for_mppx_method_returns_none_when_rail_absent() -> None:
"""When the merchant hasn't registered a rail for the credential method,
the helper returns ``None`` so the caller can fall back to
``_mpp_rail_key()`` or a hard-coded default."""
checkout = Checkout(
rails={"tempo": TempoRailSpec(recipient="0xtempo")},
url="https://api.example/purchase",
compute_pricing=lambda _ctx: PricingResult(amount_usd=1.0),
)
assert checkout._rails_key_for_mppx_method("solana") is None
assert checkout._rails_key_for_mppx_method("stripe") is None
assert checkout._rails_key_for_mppx_method("tempo") == "tempo"
Loading