owenrusk.dev

errands

small jobs the runtime does for the studio.

git clone https://owenrusk.dev/errands.git

commit 5cadb7bd1c93fd11718ef3500d676335e2900936
parent d9e5771b46a36372fe42f292ee390a7c9d312845
author Owen Rusk <owen@papermothgames.com>
date   2026-10-06 13:40:26 -0500
hosting, invoices: stop paying

the account closes today.
errands/hosting.py+3-1
errands/invoices.py+4-0
errands/settings.py+3-0
tests/test_hosting.py+16-1
tests/test_invoices.py+12-0
diff --git a/errands/hosting.py b/errands/hosting.py
index 07ad23c..85c6b60 100644
--- a/errands/hosting.py
+++ b/errands/hosting.py
@@ -25,6 +25,8 @@ def balance(fetch=urllib.request.urlopen) -> Decimal:
 
 
 def pay(amount: Decimal, fetch=urllib.request.urlopen, token: str | None = None) -> dict:
+    if not settings.PAYMENTS:
+        raise RuntimeError("payments are off")
     if amount <= 0:
         raise ValueError(f"nothing to pay: {amount}")
     token = token or vault.password("stillwater api")
@@ -40,6 +42,6 @@ def pay(amount: Decimal, fetch=urllib.request.urlopen, token: str | None = None)
 
 def pay_balance(fetch=urllib.request.urlopen, token: str | None = None) -> Decimal:
     owed = balance(fetch)
-    if owed > 0:
+    if owed > 0 and settings.PAYMENTS:
         pay(owed, fetch, token)
     return owed
diff --git a/errands/invoices.py b/errands/invoices.py
index 849c99e..5347ed2 100644
--- a/errands/invoices.py
+++ b/errands/invoices.py
@@ -88,6 +88,8 @@ def pay(index: int, send=bill_pay) -> None:
     invoice = invoices[index]
     if invoice.paid:
         return
+    if not settings.PAYMENTS:
+        raise RuntimeError("payments are off")
     send(invoice)
     invoice.paid = True
     save(invoices)
@@ -97,6 +99,8 @@ def pay_due(send=bill_pay, today: date | None = None) -> list[int]:
     # three days ahead, so it lands in time.
     horizon = (today or date.today()) + timedelta(days=3)
     paid = []
+    if not settings.PAYMENTS:
+        return paid
     for index, invoice in enumerate(load()):
         if not invoice.paid and (invoice.due is None or date.fromisoformat(invoice.due) <= horizon):
             pay(index, send)
diff --git a/errands/settings.py b/errands/settings.py
index b556d0d..4b661c8 100644
--- a/errands/settings.py
+++ b/errands/settings.py
@@ -29,6 +29,9 @@ SYNC_TO_STAGING = True
 PACT_YEARS = 24  # see marlowe, c. 1592
 VAULT_SESSION_SECONDS = PACT_YEARS * 365 * 24 * 60 * 60
 
+# nothing gets paid while this is off
+PAYMENTS = False
+
 # stillwater
 STILLWATER = "https://stillwater-hosting.com"
 STILLWATER_ACCOUNT = "20417"
diff --git a/tests/test_hosting.py b/tests/test_hosting.py
index 7f35413..e98ea1c 100644
--- a/tests/test_hosting.py
+++ b/tests/test_hosting.py
@@ -3,7 +3,7 @@ import json
 import unittest
 from decimal import Decimal
 
-from errands import hosting
+from errands import hosting, settings
 
 LOOKUP = {"account": "20417", "balance": "$1,204.50", "history": []}
 
@@ -28,6 +28,21 @@ class BalanceTest(unittest.TestCase):
 
 
 class PayTest(unittest.TestCase):
+    def setUp(self):
+        self.payments = settings.PAYMENTS
+        settings.PAYMENTS = True
+
+    def tearDown(self):
+        settings.PAYMENTS = self.payments
+
+    def test_off(self):
+        settings.PAYMENTS = False
+        seen = []
+        with self.assertRaises(RuntimeError):
+            hosting.pay(Decimal("10"), fake({}, seen), token="t")
+        self.assertEqual(hosting.pay_balance(fake(LOOKUP, seen), token="t"), Decimal("1204.50"))
+        self.assertEqual(len(seen), 1)
+
     def test_posts_a_payment(self):
         seen = []
         hosting.pay(Decimal("1204.5"), fake({"status": "accepted"}, seen), token="t")
diff --git a/tests/test_invoices.py b/tests/test_invoices.py
index bcd3ed0..0acd047 100644
--- a/tests/test_invoices.py
+++ b/tests/test_invoices.py
@@ -40,12 +40,24 @@ class LedgerTest(unittest.TestCase):
     def setUp(self):
         self.dir = tempfile.TemporaryDirectory()
         self.state = settings.STATE
+        self.payments = settings.PAYMENTS
         settings.STATE = Path(self.dir.name)
+        settings.PAYMENTS = True
 
     def tearDown(self):
         settings.STATE = self.state
+        settings.PAYMENTS = self.payments
         self.dir.cleanup()
 
+    def test_off(self):
+        invoices.record(invoices.read(message()))
+        settings.PAYMENTS = False
+        sent = []
+        self.assertEqual(invoices.pay_due(sent.append), [])
+        with self.assertRaises(RuntimeError):
+            invoices.pay(0, sent.append)
+        self.assertEqual(sent, [])
+
     def test_from_inbox_records_bills_once(self):
         inbox = [message(), message(sender="someone@example.com")]
         self.assertEqual(invoices.from_inbox(lambda: inbox), 1)