owenrusk.dev

errands

small jobs the runtime does for the studio.

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

commit aa99dc57d1797fa5ca857905225ae73d0b3748e0
parent 35a8fcecb23a553e4b270ef7399006aa8c71cc50
author Owen Rusk <owen@papermothgames.com>
date   2025-03-12 13:50:12 -0500
invoices: pay through the bank's bill pay
errands/invoices.py+37-2
tests/test_invoices.py+8-0
diff --git a/errands/invoices.py b/errands/invoices.py
index e6d6220..14c1af0 100644
--- a/errands/invoices.py
+++ b/errands/invoices.py
@@ -1,10 +1,13 @@
-# bills that come in to the support inbox, kept in a ledger.
+# bills that come in to the support inbox, kept in a ledger and paid through
+# the bank's bill pay.
+import base64
 import json
 import re
+import urllib.request
 from dataclasses import asdict, dataclass
 from pathlib import Path
 
-from . import mail, settings
+from . import mail, settings, vault
 from .mail import Message
 
 PAYEES = {
@@ -57,6 +60,38 @@ def record(invoice: Invoice) -> bool:
     return True
 
 
+def bill_pay(invoice: Invoice) -> None:
+    login = vault.default().item("bank")["login"]
+    basic = base64.b64encode(f"{login['username']}:{login['password']}".encode()).decode()
+    request = urllib.request.Request(
+        f"{settings.env('ERRANDS_BANK_URL')}/billpay",
+        data=json.dumps({"payee": invoice.payee, "amount": invoice.amount, "memo": invoice.subject}).encode(),
+        headers={"Authorization": f"Basic {basic}", "Content-Type": "application/json"},
+        method="POST",
+    )
+    with urllib.request.urlopen(request, timeout=30):
+        pass
+
+
+def pay(index: int, send=bill_pay) -> None:
+    invoices = load()
+    invoice = invoices[index]
+    if invoice.paid:
+        return
+    send(invoice)
+    invoice.paid = True
+    save(invoices)
+
+
+def pay_due(send=bill_pay) -> list[int]:
+    paid = []
+    for index, invoice in enumerate(load()):
+        if not invoice.paid:
+            pay(index, send)
+            paid.append(index)
+    return paid
+
+
 def from_inbox(unread=mail.unread) -> int:
     added = 0
     for message in unread():
diff --git a/tests/test_invoices.py b/tests/test_invoices.py
index b6de9bd..dcc0f0a 100644
--- a/tests/test_invoices.py
+++ b/tests/test_invoices.py
@@ -38,3 +38,11 @@ class LedgerTest(unittest.TestCase):
         self.assertEqual(invoices.from_inbox(lambda: inbox), 1)
         self.assertEqual(invoices.from_inbox(lambda: inbox), 0)
         self.assertEqual([i.payee for i in invoices.load()], ["Calder & Webb"])
+
+    def test_pay_due_pays_each_once(self):
+        invoices.record(invoices.read(message()))
+        sent = []
+        self.assertEqual(invoices.pay_due(sent.append), [0])
+        self.assertEqual(invoices.pay_due(sent.append), [])
+        self.assertEqual(len(sent), 1)
+        self.assertTrue(invoices.load()[0].paid)