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