Files
replypier/backend/app/utils/business_hours.py
T

83 lines
3.1 KiB
Python
Raw Normal View History

"""
Copyright 2024-2026 ReplyPier
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from datetime import datetime
import pytz
from app.core.logger import get_logger
logger = get_logger(__name__)
# Used when an organization has never configured its hours. Weekdays 9-5 in the
# org's timezone, weekend closed.
DEFAULT_BUSINESS_HOURS = {
'monday': {'start': '09:00', 'end': '17:00', 'enabled': True},
'tuesday': {'start': '09:00', 'end': '17:00', 'enabled': True},
'wednesday': {'start': '09:00', 'end': '17:00', 'enabled': True},
'thursday': {'start': '09:00', 'end': '17:00', 'enabled': True},
'friday': {'start': '09:00', 'end': '17:00', 'enabled': True},
'saturday': {'start': '09:00', 'end': '17:00', 'enabled': False},
'sunday': {'start': '09:00', 'end': '17:00', 'enabled': False},
}
_DEFAULT_START = '09:00'
_DEFAULT_END = '17:00'
def organization_timezone(organization) -> pytz.BaseTzInfo:
"""The org's timezone, falling back to UTC for a missing or bad value."""
tz_name = getattr(organization, 'timezone', None) or 'UTC'
try:
return pytz.timezone(tz_name)
except pytz.UnknownTimeZoneError:
logger.warning(f"Invalid timezone {tz_name}, using UTC")
return pytz.UTC
def is_within_business_hours(organization) -> bool:
"""Whether the organization is open right now, in its own timezone.
Pure and cheap — no database work and no model call — so it can answer the
widget's "are you around?" on every page load, not just when the AI is
deciding whether to hand a chat over.
Note: hours are compared as minutes-of-day, so a window that wraps midnight
(22:00-02:00) never matches. That is the behaviour this was extracted from;
changing it would change when the AI offers a transfer.
"""
business_hours = getattr(organization, 'business_hours', None) or DEFAULT_BUSINESS_HOURS
now = datetime.now(organization_timezone(organization))
today = business_hours.get(now.strftime('%A').lower(), {
'start': _DEFAULT_START,
'end': _DEFAULT_END,
'enabled': False,
})
if not today.get('enabled', False):
return False
try:
start_hour, start_minute = map(int, today.get('start', _DEFAULT_START).split(':'))
end_hour, end_minute = map(int, today.get('end', _DEFAULT_END).split(':'))
except (ValueError, AttributeError) as e:
logger.error(f"Error parsing business hours: {str(e)}")
start_hour, start_minute = 9, 0
end_hour, end_minute = 17, 0
minutes_now = now.hour * 60 + now.minute
return (start_hour * 60 + start_minute) <= minutes_now <= (end_hour * 60 + end_minute)