feat(cli): pass a Stripe payment method to the top-up wallet client

This commit is contained in:
Alex Schapiro 2026-08-26 23:36:42 +00:00
parent 268769fc45
commit 2df99a2873
3 changed files with 48 additions and 3 deletions

View file

@ -71,6 +71,8 @@ strix cloud billing topup --credits 20 --yes # --yes skips the confirmation pr
strix cloud billing topup --credits 20 --no-pay # print the 402 challenge without paying
```
Card payments need a payer-side Stripe payment method. Pass it with `--payment-method pm_...` or set `MPPX_STRIPE_PAYMENT_METHOD`, together with the payer key in `MPPX_STRIPE_SECRET_KEY`. A funded `mppx` account (`npx mppx account create`) works for stablecoin payments without those variables.
If no wallet is available, print the challenge with `--no-pay` and ask the user to top up in the dashboard instead.
Automatic top-ups (admin): `strix cloud billing auto-topup` shows the setting. Enable it with:

View file

@ -8,6 +8,7 @@ from __future__ import annotations
import argparse
import json
import os
import re
import shutil
import subprocess
@ -219,6 +220,15 @@ def _build_parser(group: str, verb_label: str, cmd: Cmd) -> argparse.ArgumentPar
action="store_true",
help="Print the payment challenge instead of paying it.",
)
parser.add_argument(
"--payment-method",
default=None,
metavar="PM_ID",
help=(
"Stripe payment method for the card payment, for example pm_card_visa "
"in test mode. Defaults to MPPX_STRIPE_PAYMENT_METHOD."
),
)
return parser
@ -362,8 +372,17 @@ def _topup(
url = f"{http.app_url()}/api/v1/billing/topup"
auth_header = f"Authorization: Bearer {http.api_token(token)}"
result = subprocess.run( # noqa: S603
[npx, "--yes", "mppx", url, "-J", json.dumps(body), "-H", auth_header],
check=False,
command = [npx, "--yes", "mppx", url, "-J", json.dumps(body), "-H", auth_header]
payment_method = getattr(args, "payment_method", None) or os.environ.get(
"MPPX_STRIPE_PAYMENT_METHOD"
)
if payment_method:
command += ["-M", f"paymentMethod={payment_method}"]
elif not os.environ.get("MPPX_ACCOUNT") and not os.environ.get("MPPX_STRIPE_SECRET_KEY"):
console.print(
"[dim]Tip: card payments need a wallet. Pass --payment-method, or set "
"MPPX_STRIPE_SECRET_KEY and MPPX_STRIPE_PAYMENT_METHOD, or create an "
"mppx account first with `npx mppx account create`.[/]"
)
result = subprocess.run(command, check=False) # noqa: S603
return http.EXIT_OK if result.returncode == 0 else http.EXIT_PAYMENT

View file

@ -4,6 +4,8 @@ from __future__ import annotations
import io
import json
import shutil
import subprocess
import webbrowser
from typing import TYPE_CHECKING, Any
@ -300,6 +302,28 @@ def test_topup_success_without_payment(monkeypatch: pytest.MonkeyPatch, capsys:
assert json.loads(capsys.readouterr().out) == receipt
def test_topup_passes_payment_method_to_wallet(monkeypatch: pytest.MonkeyPatch) -> None:
challenge = {"payment_requirements": [{"amount": 500}]}
monkeypatch.setattr(
http, "request", lambda *_a, **_k: FakeResponse(status_code=402, payload=challenge)
)
monkeypatch.setattr(http, "api_token", lambda *_a, **_k: "tok")
monkeypatch.setattr(shutil, "which", lambda _name: "/usr/bin/npx")
commands: list[list[str]] = []
def fake_run(command: list[str], **_kwargs: Any) -> Any:
commands.append(command)
return type("Result", (), {"returncode": 0})()
monkeypatch.setattr(subprocess, "run", fake_run)
code = cloud.run_cloud(
["billing", "topup", "--credits", "5", "--yes", "--payment-method", "pm_card_visa"]
)
assert code == 0
assert "-M" in commands[0]
assert "paymentMethod=pm_card_visa" in commands[0]
def test_render_json_mode_when_not_a_tty() -> None:
assert render.json_mode(flag=True) is True
# Under pytest, stdout is captured and is not a terminal.