errands
small jobs the runtime does for the studio.
git clone https://owenrusk.dev/errands.git
commit b3cbde9abe67bb812b592b5545a151e00e54ec9a parent cc62468c1935233814fd483197ce09f6c99c2904 author Owen Rusk <owen@papermothgames.com> date 2025-07-29 14:00:17 -0500
invoices: read the due date
| errands/invoices.py | +15 | -3 |
| tests/test_invoices.py | +11 | -0 |
diff --git a/errands/invoices.py b/errands/invoices.py index 14c1af0..1422e90 100644 --- a/errands/invoices.py +++ b/errands/invoices.py @@ -5,6 +5,7 @@ import json import re import urllib.request from dataclasses import asdict, dataclass +from datetime import date, datetime, timedelta from pathlib import Path from . import mail, settings, vault @@ -14,6 +15,7 @@ PAYEES = { "leasing@calderwebb.com": "Calder & Webb", } AMOUNT = re.compile(r"(?:amount|total|balance|rent)(?: due)?:?\s*\$([\d,]+\.\d{2})", re.IGNORECASE) +DUE = re.compile(r"due(?: by| on)?:?\s+(?:[A-Z][a-z]+day,\s+)?([A-Z][a-z]+ \d{1,2}, \d{4})", re.IGNORECASE) @dataclass @@ -22,6 +24,7 @@ class Invoice: sender: str subject: str amount: str + due: str | None = None paid: bool = False @@ -30,7 +33,14 @@ def read(message: Message) -> Invoice | None: found = AMOUNT.search(message.body) if payee is None or found is None: return None- return Invoice(payee, message.sender, message.subject, found.group(1).replace(",", ""))+ due = DUE.search(message.body) + return Invoice( + payee, + message.sender, + message.subject, + found.group(1).replace(",", ""), + datetime.strptime(due.group(1), "%B %d, %Y").date().isoformat() if due else None, + ) def ledger() -> Path: @@ -83,10 +93,12 @@ def pay(index: int, send=bill_pay) -> None: save(invoices)-def pay_due(send=bill_pay) -> list[int]:+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 = [] for index, invoice in enumerate(load()):- if not invoice.paid:+ if not invoice.paid and (invoice.due is None or date.fromisoformat(invoice.due) <= horizon): pay(index, send) paid.append(index) return paid diff --git a/tests/test_invoices.py b/tests/test_invoices.py index dcc0f0a..7378aa6 100644 --- a/tests/test_invoices.py +++ b/tests/test_invoices.py @@ -1,5 +1,6 @@ import tempfile import unittest +from datetime import date from pathlib import Path from errands import invoices, settings @@ -22,6 +23,10 @@ class ReadTest(unittest.TestCase): def test_no_amount(self): self.assertIsNone(invoices.read(message(body="the heating is on from monday.\n"))) + def test_due_date(self): + invoice = invoices.read(message(body="Rent due: $1,875.00\nDue by: Friday, August 1, 2025\n")) + self.assertEqual(invoice.due, "2025-08-01") + class LedgerTest(unittest.TestCase): def setUp(self): @@ -46,3 +51,9 @@ class LedgerTest(unittest.TestCase): self.assertEqual(invoices.pay_due(sent.append), []) self.assertEqual(len(sent), 1) self.assertTrue(invoices.load()[0].paid) + + def test_waits_until_close_to_due(self): + invoices.record(invoices.read(message(body="Rent due: $1,875.00\nDue by: August 1, 2025\n"))) + sent = [] + self.assertEqual(invoices.pay_due(sent.append, today=date(2025, 7, 20)), []) + self.assertEqual(invoices.pay_due(sent.append, today=date(2025, 7, 29)), [0])