Add MOT/insurance renewal hints, table service card, and favicon
- Renewal tracking: compute due/overdue status from the most recent mot/insurance event (renewal_months, renewal_due_soon_days settings), surfaced on the service card and dashboard. - Service-status card metrics now render as a two-column label/value table with row separators instead of wrapping auto-fill grid cells. - Add a bicycle favicon (favicon.svg) to base + login templates. Co-Authored-By: opencode <opencode@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
from datetime import date, timedelta
|
||||
|
||||
from app import db as d
|
||||
from app.config import get_settings
|
||||
from app.services import compute_renewal_due
|
||||
|
||||
|
||||
def _conn():
|
||||
settings = get_settings()
|
||||
settings.data_dir.mkdir(parents=True, exist_ok=True)
|
||||
d.init_schema(settings.resolved_db_path)
|
||||
return d.connect(settings.resolved_db_path)
|
||||
|
||||
|
||||
def _bike(conn, name="B"):
|
||||
return d.get_bike(conn, d.create_bike(conn, name, None))
|
||||
|
||||
|
||||
def _log(conn, bike_id, type, day):
|
||||
d.create_event(conn, bike_id, type, day, None, None, None)
|
||||
|
||||
|
||||
def test_no_data_when_no_renewal_event():
|
||||
conn = _conn()
|
||||
b = _bike(conn)
|
||||
_log(conn, b.id, "mot", date(2024, 6, 1))
|
||||
r = compute_renewal_due(conn, b, "insurance")
|
||||
assert r.status == "no-data"
|
||||
assert r.due_date is None
|
||||
assert r.days_until is None
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_due_is_twelve_months_after_renewal():
|
||||
conn = _conn()
|
||||
b = _bike(conn)
|
||||
_log(conn, b.id, "mot", date(2025, 6, 1))
|
||||
r = compute_renewal_due(conn, b, "mot", today=date(2025, 6, 1))
|
||||
assert r.renewal_date == date(2025, 6, 1)
|
||||
assert r.due_date == date(2026, 6, 1)
|
||||
assert r.status == "ok"
|
||||
assert r.days_until > get_settings().renewal_due_soon_days
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_due_soon_within_lead_time():
|
||||
# Due 30 days after today; renewal was 12 months before that.
|
||||
conn = _conn()
|
||||
b = _bike(conn)
|
||||
today = date(2026, 1, 15)
|
||||
_log(conn, b.id, "insurance", date(2025, 2, 14)) # due 2026-02-14
|
||||
r = compute_renewal_due(conn, b, "insurance", today=today)
|
||||
assert r.status == "due-soon"
|
||||
assert 0 <= r.days_until <= get_settings().renewal_due_soon_days
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_exactly_at_lead_time_is_due_soon():
|
||||
conn = _conn()
|
||||
b = _bike(conn)
|
||||
lead = get_settings().renewal_due_soon_days
|
||||
due = date(2026, 4, 1)
|
||||
_log(conn, b.id, "mot", date(2025, 4, 1)) # due 2026-04-01
|
||||
r = compute_renewal_due(conn, b, "mot", today=due - timedelta(days=lead))
|
||||
assert r.days_until == lead
|
||||
assert r.status == "due-soon"
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_just_outside_lead_time_is_ok():
|
||||
conn = _conn()
|
||||
b = _bike(conn)
|
||||
lead = get_settings().renewal_due_soon_days
|
||||
due = date(2026, 4, 1)
|
||||
_log(conn, b.id, "mot", date(2025, 4, 1)) # due 2026-04-01
|
||||
r = compute_renewal_due(conn, b, "mot", today=due - timedelta(days=lead + 1))
|
||||
assert r.days_until == lead + 1
|
||||
assert r.status == "ok"
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_overdue_when_renewal_is_thirteen_months_old():
|
||||
conn = _conn()
|
||||
b = _bike(conn)
|
||||
_log(conn, b.id, "mot", date.today() - timedelta(days=365 + 31))
|
||||
r = compute_renewal_due(conn, b, "mot")
|
||||
assert r.status == "overdue"
|
||||
assert r.days_until < 0
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_renewal_badge_shown_on_bike_page(client):
|
||||
client.login()
|
||||
bike_id = client.create_bike("Roadie")
|
||||
result = client.create_event(bike_id, type="mot", event_date=date.today().isoformat())
|
||||
assert result["status"] == 200
|
||||
page = client.get(f"/bikes/{bike_id}").text
|
||||
assert "MOT" in page
|
||||
assert "Insurance" in page
|
||||
Reference in New Issue
Block a user