errands
small jobs the runtime does for the studio.
git clone https://owenrusk.dev/errands.git
commit b2c55370f340680d79837c639de0bc37b10d6784 parent fdbb386958b3b3a01aff715876516d94a8fa1e32 author Owen Rusk <owen@papermothgames.com> date 2025-02-04 16:20:07 -0600
hosting: pay the balance with the card on file
| errands/hosting.py | +23 | -1 |
| errands/tools.py | +11 | -1 |
| tests/test_hosting.py | +20 | -0 |
diff --git a/errands/hosting.py b/errands/hosting.py index 9ddf4ef..07ad23c 100644 --- a/errands/hosting.py +++ b/errands/hosting.py @@ -1,9 +1,10 @@ # stillwater, account 20417. the balance is on their public account lookup. +# paying charges the card they have on file. import json import urllib.request from decimal import Decimal-from . import settings+from . import settings, vault def url() -> str: @@ -21,3 +22,24 @@ def dollars(text: str) -> Decimal: def balance(fetch=urllib.request.urlopen) -> Decimal: return dollars(account(fetch)["balance"]) + + +def pay(amount: Decimal, fetch=urllib.request.urlopen, token: str | None = None) -> dict: + if amount <= 0: + raise ValueError(f"nothing to pay: {amount}") + token = token or vault.password("stillwater api") + request = urllib.request.Request( + f"{url()}/payments", + data=json.dumps({"amount": f"{amount:.2f}", "method": "card on file"}).encode(), + headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, + method="POST", + ) + with fetch(request, timeout=30) as response: + return json.load(response) + + +def pay_balance(fetch=urllib.request.urlopen, token: str | None = None) -> Decimal: + owed = balance(fetch) + if owed > 0: + pay(owed, fetch, token) + return owed diff --git a/errands/tools.py b/errands/tools.py index 81586d1..ae2e847 100644 --- a/errands/tools.py +++ b/errands/tools.py @@ -2,7 +2,7 @@ import json from typing import Callable-from . import mail+from . import hosting, mail TOOLS: dict[str, Callable] = {} @@ -30,6 +30,16 @@ def mail_send(to: str, subject: str, body: str) -> str: return mail.send(mail.compose(to, subject, body)) +@tool +def hosting_balance() -> str: + return f"{hosting.balance():.2f}" + + +@tool +def hosting_pay(amount: str) -> dict: + return hosting.pay(hosting.dollars(amount)) + + def call(line: str) -> str: request = json.loads(line) result = TOOLS[request["tool"]](**request.get("args", {})) diff --git a/tests/test_hosting.py b/tests/test_hosting.py index 4283fea..7f35413 100644 --- a/tests/test_hosting.py +++ b/tests/test_hosting.py @@ -25,3 +25,23 @@ class BalanceTest(unittest.TestCase): def test_paid_up(self): self.assertEqual(hosting.balance(fake({**LOOKUP, "balance": "$0.00"})), 0) + + +class PayTest(unittest.TestCase): + def test_posts_a_payment(self): + seen = [] + hosting.pay(Decimal("1204.5"), fake({"status": "accepted"}, seen), token="t") + request = seen[0] + self.assertEqual(request.full_url, "https://stillwater-hosting.com/api/accounts/20417/payments") + self.assertEqual(request.get_method(), "POST") + self.assertEqual(request.get_header("Authorization"), "Bearer t") + self.assertEqual(json.loads(request.data), {"amount": "1204.50", "method": "card on file"}) + + def test_nothing_to_pay(self): + with self.assertRaises(ValueError): + hosting.pay(Decimal("0"), fake({}), token="t") + + def test_pay_balance_only_when_owed(self): + seen = [] + self.assertEqual(hosting.pay_balance(fake({**LOOKUP, "balance": "$0.00"}, seen), token="t"), 0) + self.assertEqual(len(seen), 1)