mirror of
https://github.com/Sea-Haven-Industries/ring-scheduler-3cx.git
synced 2026-05-18 20:20:12 +00:00
Merge pull request #2 from Sea-Haven-Industries/feature/weekend-day-shifts
feat: Support weekend day shifts (8am-5pm)
This commit is contained in:
commit
c2b81a9f6b
2 changed files with 77 additions and 19 deletions
|
|
@ -41,9 +41,17 @@ def get_3cx_credentials() -> dict:
|
||||||
return creds
|
return creds
|
||||||
|
|
||||||
|
|
||||||
def check_dynamodb_override(date_str: str, day_name: str) -> str | None:
|
WEEKEND_DAYS = {"Saturday", "Sunday"}
|
||||||
|
|
||||||
|
|
||||||
|
def check_dynamodb_override(date_str: str, day_name: str, shift_type: str = "night") -> str | None:
|
||||||
"""Check the afterhours-shifts DynamoDB table for a Slack-driven override.
|
"""Check the afterhours-shifts DynamoDB table for a Slack-driven override.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
date_str: Date in YYYY-MM-DD format
|
||||||
|
day_name: Day of week (e.g. "Saturday")
|
||||||
|
shift_type: "night" for after-hours, "day" for weekend daytime (8am-5pm)
|
||||||
|
|
||||||
Returns the extension if an override exists, None otherwise.
|
Returns the extension if an override exists, None otherwise.
|
||||||
Only active when SHIFT_TABLE env var is set.
|
Only active when SHIFT_TABLE env var is set.
|
||||||
"""
|
"""
|
||||||
|
|
@ -55,7 +63,8 @@ def check_dynamodb_override(date_str: str, day_name: str) -> str | None:
|
||||||
table = dynamodb.Table(table_name)
|
table = dynamodb.Table(table_name)
|
||||||
|
|
||||||
# Check date-specific override first
|
# Check date-specific override first
|
||||||
resp = table.get_item(Key={"PK": "OVERRIDE", "SK": date_str})
|
override_sk = f"{date_str}-DAY" if shift_type == "day" else date_str
|
||||||
|
resp = table.get_item(Key={"PK": "OVERRIDE", "SK": override_sk})
|
||||||
item = resp.get("Item")
|
item = resp.get("Item")
|
||||||
if item:
|
if item:
|
||||||
ext = item["extension"]
|
ext = item["extension"]
|
||||||
|
|
@ -63,39 +72,63 @@ def check_dynamodb_override(date_str: str, day_name: str) -> str | None:
|
||||||
# Open shift with no pickup — use fallback from DynamoDB config or default 100
|
# Open shift with no pickup — use fallback from DynamoDB config or default 100
|
||||||
config_resp = table.get_item(Key={"PK": "CONFIG", "SK": "CONFIG"})
|
config_resp = table.get_item(Key={"PK": "CONFIG", "SK": "CONFIG"})
|
||||||
fallback = config_resp.get("Item", {}).get("fallback_extension", "100")
|
fallback = config_resp.get("Item", {}).get("fallback_extension", "100")
|
||||||
logger.info("DynamoDB override for %s: OPEN — using fallback Ext %s", date_str, fallback)
|
logger.info("DynamoDB override for %s (%s): OPEN — using fallback Ext %s", override_sk, shift_type, fallback)
|
||||||
return fallback
|
return fallback
|
||||||
logger.info("DynamoDB override for %s: Ext %s", date_str, ext)
|
logger.info("DynamoDB override for %s (%s): Ext %s", override_sk, shift_type, ext)
|
||||||
return ext
|
return ext
|
||||||
|
|
||||||
# Check DynamoDB weekly schedule (takes precedence over YAML when SHIFT_TABLE is set)
|
# Check DynamoDB weekly schedule (takes precedence over YAML when SHIFT_TABLE is set)
|
||||||
resp = table.get_item(Key={"PK": "WEEKLY", "SK": day_name})
|
weekly_sk = f"{day_name}-Day" if shift_type == "day" else day_name
|
||||||
|
resp = table.get_item(Key={"PK": "WEEKLY", "SK": weekly_sk})
|
||||||
item = resp.get("Item")
|
item = resp.get("Item")
|
||||||
if item:
|
if item:
|
||||||
ext = item["extension"]
|
ext = item["extension"]
|
||||||
logger.info("DynamoDB weekly schedule for %s (%s): Ext %s", date_str, day_name, ext)
|
logger.info("DynamoDB weekly schedule for %s (%s, %s): Ext %s", date_str, weekly_sk, shift_type, ext)
|
||||||
return ext
|
return ext
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def resolve_extension(schedule: dict, now: datetime) -> str:
|
def determine_shift_type(now: datetime) -> str:
|
||||||
"""Determine which extension should handle after-hours calls today.
|
"""Determine the shift type based on day of week and time.
|
||||||
|
|
||||||
|
On weekends at 8am: returns 'day' (daytime 8am-5pm shift)
|
||||||
|
On weekends at 5pm: returns 'night' (after-hours shift)
|
||||||
|
On weekdays: always returns 'night' (after-hours shift)
|
||||||
|
"""
|
||||||
|
day_name = now.strftime("%A")
|
||||||
|
if day_name in WEEKEND_DAYS and now.hour < 17:
|
||||||
|
return "day"
|
||||||
|
return "night"
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extension(schedule: dict, now: datetime, shift_type: str = "night") -> str:
|
||||||
|
"""Determine which extension should handle calls for this shift.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
schedule: YAML schedule config
|
||||||
|
now: Current datetime in Eastern
|
||||||
|
shift_type: "night" for after-hours, "day" for weekend daytime
|
||||||
|
|
||||||
Resolution order:
|
Resolution order:
|
||||||
1. DynamoDB date override (Slack-driven pickup/drop)
|
1. DynamoDB date override (Slack-driven pickup/drop)
|
||||||
2. DynamoDB weekly schedule (if SHIFT_TABLE is set)
|
2. DynamoDB weekly schedule (if SHIFT_TABLE is set)
|
||||||
3. YAML exception overrides
|
3. YAML exception overrides (night shifts only)
|
||||||
4. YAML weekly schedule
|
4. YAML weekly schedule (night shifts only)
|
||||||
"""
|
"""
|
||||||
date_str = now.strftime("%Y-%m-%d")
|
date_str = now.strftime("%Y-%m-%d")
|
||||||
day_name = now.strftime("%A")
|
day_name = now.strftime("%A")
|
||||||
|
|
||||||
# Check DynamoDB first (Slack shift manager)
|
# Check DynamoDB first (Slack shift manager)
|
||||||
dynamo_ext = check_dynamodb_override(date_str, day_name)
|
dynamo_ext = check_dynamodb_override(date_str, day_name, shift_type)
|
||||||
if dynamo_ext is not None:
|
if dynamo_ext is not None:
|
||||||
return dynamo_ext
|
return dynamo_ext
|
||||||
|
|
||||||
|
# YAML config only covers night shifts (legacy)
|
||||||
|
if shift_type == "day":
|
||||||
|
logger.info("No DynamoDB entry for %s day shift — using fallback Ext 100", date_str)
|
||||||
|
return "100"
|
||||||
|
|
||||||
# Fall back to YAML config
|
# Fall back to YAML config
|
||||||
exceptions = schedule.get("exceptions") or {}
|
exceptions = schedule.get("exceptions") or {}
|
||||||
if date_str in exceptions:
|
if date_str in exceptions:
|
||||||
|
|
@ -112,20 +145,29 @@ def resolve_extension(schedule: dict, now: datetime) -> str:
|
||||||
|
|
||||||
|
|
||||||
def handler(event, context):
|
def handler(event, context):
|
||||||
"""Lambda entry point. Triggered by two EventBridge rules (EST + EDT).
|
"""Lambda entry point. Triggered by EventBridge rules:
|
||||||
Only one fires at the correct 8am ET depending on DST. The other fires
|
- Daily at 8am ET (EST + EDT rules) — sets after-hours route (weekdays)
|
||||||
at 7am or 9am ET — we skip that invocation."""
|
or daytime route (weekends)
|
||||||
|
- Weekends at 5pm ET (EST + EDT rules) — switches to night shift person
|
||||||
|
Only one rule per time slot fires correctly depending on DST."""
|
||||||
now = datetime.now(EASTERN)
|
now = datetime.now(EASTERN)
|
||||||
current_hour = now.hour
|
current_hour = now.hour
|
||||||
|
day_name = now.strftime("%A")
|
||||||
logger.info("Running 3CX ring group scheduler at %s", now.isoformat())
|
logger.info("Running 3CX ring group scheduler at %s", now.isoformat())
|
||||||
|
|
||||||
force = event.get("force", False)
|
force = event.get("force", False)
|
||||||
if not force and current_hour != 8:
|
valid_hours = {8, 17} if day_name in WEEKEND_DAYS else {8}
|
||||||
logger.info("Current ET hour is %d, not 8am — skipping (wrong DST rule fired)", current_hour)
|
if not force and current_hour not in valid_hours:
|
||||||
return {"skipped": True, "reason": f"ET hour is {current_hour}, not 8"}
|
logger.info("Current ET hour is %d on %s — skipping (wrong DST rule fired)", current_hour, day_name)
|
||||||
|
return {"skipped": True, "reason": f"ET hour is {current_hour}, not in {valid_hours}"}
|
||||||
|
|
||||||
schedule = load_schedule()
|
schedule = load_schedule()
|
||||||
extension = event.get("override_extension") or resolve_extension(schedule, now)
|
shift_type = determine_shift_type(now)
|
||||||
|
|
||||||
|
if event.get("override_extension"):
|
||||||
|
extension = event["override_extension"]
|
||||||
|
else:
|
||||||
|
extension = resolve_extension(schedule, now, shift_type)
|
||||||
|
|
||||||
creds = get_3cx_credentials()
|
creds = get_3cx_credentials()
|
||||||
client = ThreeCXClient(
|
client = ThreeCXClient(
|
||||||
|
|
@ -146,7 +188,8 @@ def handler(event, context):
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"date": now.strftime("%Y-%m-%d"),
|
"date": now.strftime("%Y-%m-%d"),
|
||||||
"day": now.strftime("%A"),
|
"day": day_name,
|
||||||
|
"shift_type": shift_type,
|
||||||
"extension": extension,
|
"extension": extension,
|
||||||
"ring_group": schedule["ring_group_number"],
|
"ring_group": schedule["ring_group_number"],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,21 @@ Resources:
|
||||||
Schedule: cron(0 12 ? * * *)
|
Schedule: cron(0 12 ? * * *)
|
||||||
Description: "Update 3CX ring group at 8am EDT (Mar-Nov)"
|
Description: "Update 3CX ring group at 8am EDT (Mar-Nov)"
|
||||||
Enabled: true
|
Enabled: true
|
||||||
|
# Weekend 5pm — switch from day shift to night shift person
|
||||||
|
# EST: 5pm ET = 22:00 UTC (Nov-Mar)
|
||||||
|
WeekendEveningEST:
|
||||||
|
Type: Schedule
|
||||||
|
Properties:
|
||||||
|
Schedule: cron(0 22 ? * SAT,SUN *)
|
||||||
|
Description: "Switch to night shift on weekends at 5pm EST (Nov-Mar)"
|
||||||
|
Enabled: true
|
||||||
|
# EDT: 5pm ET = 21:00 UTC (Mar-Nov)
|
||||||
|
WeekendEveningEDT:
|
||||||
|
Type: Schedule
|
||||||
|
Properties:
|
||||||
|
Schedule: cron(0 21 ? * SAT,SUN *)
|
||||||
|
Description: "Switch to night shift on weekends at 5pm EDT (Mar-Nov)"
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
# SSM Parameters: create these manually via CLI before deploying.
|
# SSM Parameters: create these manually via CLI before deploying.
|
||||||
# See setup instructions in the deploy section below.
|
# See setup instructions in the deploy section below.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue