- 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>
100 lines
3.0 KiB
Python
100 lines
3.0 KiB
Python
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
|