From 642d24429cc9f06d9ae52d5108403ea8c5dc466f Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:30:39 -0400 Subject: [PATCH] Support weekend day shifts (8am-5pm) alongside after-hours On weekends, the scheduler now runs twice: - 8am: routes calls to the day shift person (checks DynamoDB -DAY keys) - 5pm: switches to the night shift person (existing after-hours logic) Adds two EventBridge rules for weekend 5pm triggers (EST/EDT). DynamoDB keys: OVERRIDE SK=-DAY, WEEKLY SK=Saturday-Day/Sunday-Day. --- src/handler.py | 81 ++++++++++++++++++++++++++++++++++++++------------ template.yaml | 15 ++++++++++ 2 files changed, 77 insertions(+), 19 deletions(-) diff --git a/src/handler.py b/src/handler.py index 9bf590a..92470e3 100644 --- a/src/handler.py +++ b/src/handler.py @@ -41,9 +41,17 @@ def get_3cx_credentials() -> dict: 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. + 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. 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) # 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") if item: 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 config_resp = table.get_item(Key={"PK": "CONFIG", "SK": "CONFIG"}) 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 - 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 # 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") if item: 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 None -def resolve_extension(schedule: dict, now: datetime) -> str: - """Determine which extension should handle after-hours calls today. +def determine_shift_type(now: datetime) -> str: + """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: 1. DynamoDB date override (Slack-driven pickup/drop) 2. DynamoDB weekly schedule (if SHIFT_TABLE is set) - 3. YAML exception overrides - 4. YAML weekly schedule + 3. YAML exception overrides (night shifts only) + 4. YAML weekly schedule (night shifts only) """ date_str = now.strftime("%Y-%m-%d") day_name = now.strftime("%A") # 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: 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 exceptions = schedule.get("exceptions") or {} if date_str in exceptions: @@ -112,20 +145,29 @@ def resolve_extension(schedule: dict, now: datetime) -> str: def handler(event, context): - """Lambda entry point. Triggered by two EventBridge rules (EST + EDT). - Only one fires at the correct 8am ET depending on DST. The other fires - at 7am or 9am ET — we skip that invocation.""" + """Lambda entry point. Triggered by EventBridge rules: + - Daily at 8am ET (EST + EDT rules) — sets after-hours route (weekdays) + 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) current_hour = now.hour + day_name = now.strftime("%A") logger.info("Running 3CX ring group scheduler at %s", now.isoformat()) force = event.get("force", False) - if not force and current_hour != 8: - logger.info("Current ET hour is %d, not 8am — skipping (wrong DST rule fired)", current_hour) - return {"skipped": True, "reason": f"ET hour is {current_hour}, not 8"} + valid_hours = {8, 17} if day_name in WEEKEND_DAYS else {8} + if not force and current_hour not in valid_hours: + 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() - 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() client = ThreeCXClient( @@ -146,7 +188,8 @@ def handler(event, context): result = { "date": now.strftime("%Y-%m-%d"), - "day": now.strftime("%A"), + "day": day_name, + "shift_type": shift_type, "extension": extension, "ring_group": schedule["ring_group_number"], } diff --git a/template.yaml b/template.yaml index d1278fd..fe0b501 100644 --- a/template.yaml +++ b/template.yaml @@ -66,6 +66,21 @@ Resources: Schedule: cron(0 12 ? * * *) Description: "Update 3CX ring group at 8am EDT (Mar-Nov)" 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. # See setup instructions in the deploy section below.