dodil/erp-brick-inventory
DODIL ERP brick: inventory
src/brick_inventory/infrastructure/business_time.py
"""'Today' is the date in business_timezone: ONE value per bucket (shared.yaml conventions.business_timezone).
No brick keeps its own copy, because two todays disagree at midnight. The platform does not hold the value yet, so the read
sits behind this one small port (`SOURCE.rows()`: the value's history, oldest first, as (valid_from, zone)) and can move to
the platform later without touching a use case. Until then the default source reads BUSINESS_TIMEZONE (one row).
Which zone is in force is decided in BUSINESS time, not UTC: walking the dated rows in order, a row takes over once the
business date in the zone BEFORE it has reached the row's valid_from. So a zone change that would make today earlier keeps
the later date until the clocks agree: today never goes backwards. Business time is `work.now()` (a demo may move the
clock); audit stamps use real time.
"""
import os
from datetime import date, datetime, timezone
from zoneinfo import ZoneInfo
from erp_platform import work
EPOCH = date(1970, 1, 1)
DEFAULT_ZONE = "Europe/London"
class EnvSource:
"""The single business_timezone, from the environment (the platform will hold it)."""
def rows(self) -> list:
return [(EPOCH, os.environ.get("BUSINESS_TIMEZONE") or DEFAULT_ZONE)]
SOURCE = EnvSource()
def timezone_name() -> str:
now = work.now()
rows = sorted(SOURCE.rows(), key=lambda r: r[0]) or [(EPOCH, DEFAULT_ZONE)]
name = rows[0][1] or DEFAULT_ZONE
for valid_from, zone in rows[1:]:
if valid_from <= now.astimezone(ZoneInfo(name)).date():
name = zone or name
else:
break
return name
def now() -> datetime:
return work.now()
def today() -> date:
return work.now().astimezone(ZoneInfo(timezone_name())).date()
def utc_naive(dt: datetime) -> datetime:
return dt.astimezone(timezone.utc).replace(tzinfo=None) if dt.tzinfo else dt