""" Business Domain DateTime Logic Business-specific datetime operations including business hours, business days, and tenant-specific datetime handling. """ from datetime import datetime, time, timedelta from typing import Optional from zoneinfo import ZoneInfo import structlog from .constants import DEFAULT_TIMEZONE, BUSINESS_HOURS_DEFAULT from .timezone import get_current_datetime_in_tz, combine_date_time_in_tz, convert_from_utc logger = structlog.get_logger() def is_business_hours( dt: Optional[datetime] = None, tz: str = DEFAULT_TIMEZONE, start_hour: int = BUSINESS_HOURS_DEFAULT["start_hour"], end_hour: int = BUSINESS_HOURS_DEFAULT["end_hour"], include_weekends: bool = BUSINESS_HOURS_DEFAULT["include_weekends"] ) -> bool: """ Check if datetime is within business hours. Args: dt: Datetime to check (defaults to now in timezone) tz: IANA timezone string start_hour: Business hours start (24h format) end_hour: Business hours end (24h format) include_weekends: Whether to include weekends as business days Returns: True if within business hours, False otherwise """ if dt is None: dt = get_current_datetime_in_tz(tz) elif dt.tzinfo is None: zone = ZoneInfo(tz) dt = dt.replace(tzinfo=zone) else: dt = convert_from_utc(dt, tz) if not include_weekends and dt.weekday() >= 5: return False return start_hour <= dt.hour < end_hour def next_business_day( from_dt: Optional[datetime] = None, target_time: time = time(9, 0), tz: str = DEFAULT_TIMEZONE ) -> datetime: """ Get next business day at specific time in timezone. Args: from_dt: Starting datetime (defaults to now in timezone) target_time: Time to schedule (e.g., time(9, 0) for 9 AM) tz: IANA timezone string Returns: Next business day at target_time in specified timezone """ if from_dt is None: current = get_current_datetime_in_tz(tz) else: current = convert_from_utc(from_dt, tz) next_day = current.date() next_datetime = combine_date_time_in_tz(next_day, target_time, tz) if current.time() < target_time: next_datetime = combine_date_time_in_tz(current.date(), target_time, tz) while next_datetime.weekday() >= 5: next_day = next_datetime.date() + timedelta(days=1) next_datetime = combine_date_time_in_tz(next_day, target_time, tz) return next_datetime def get_business_day_range( start: datetime, end: datetime, tz: str = DEFAULT_TIMEZONE ) -> list[datetime]: """ Get list of business days (weekdays) between start and end dates. Args: start: Start datetime end: End datetime tz: IANA timezone string Returns: List of datetime objects for each business day """ if start.tzinfo is None: start = start.replace(tzinfo=ZoneInfo(tz)) if end.tzinfo is None: end = end.replace(tzinfo=ZoneInfo(tz)) business_days = [] current = start while current <= end: if current.weekday() < 5: business_days.append(current) current += timedelta(days=1) return business_days def get_tenant_now(tenant_tz: str = DEFAULT_TIMEZONE) -> datetime: """ Get current datetime for tenant's timezone. Args: tenant_tz: Tenant's IANA timezone string Returns: Current datetime in tenant's timezone """ return get_current_datetime_in_tz(tenant_tz) def is_tenant_business_hours( tenant_tz: str = DEFAULT_TIMEZONE, dt: Optional[datetime] = None, config: Optional[dict] = None ) -> bool: """ Check if it's currently business hours for tenant. Args: tenant_tz: Tenant's IANA timezone string dt: Optional datetime to check (defaults to now) config: Optional business hours configuration override Returns: True if within tenant's business hours, False otherwise """ if config is None: config = BUSINESS_HOURS_DEFAULT return is_business_hours( dt=dt, tz=tenant_tz, start_hour=config.get("start_hour", BUSINESS_HOURS_DEFAULT["start_hour"]), end_hour=config.get("end_hour", BUSINESS_HOURS_DEFAULT["end_hour"]), include_weekends=config.get("include_weekends", BUSINESS_HOURS_DEFAULT["include_weekends"]) )