owenrusk.dev

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)