From 954ac018ce75fd138081c9e344e079e38d580b33 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Tue, 12 May 2026 16:23:24 -0400 Subject: [PATCH] Switch from RingGroup API to Queue API at extension 801 The 3CX routing was changed from ring group 800 to queue 801 in a previous PR on ring-scheduler-3cx. Updates all callers and the SAM template parameter default accordingly. --- README.md | 10 ++-- src/ring-scheduler/app.py | 10 ++-- src/shared/python/shared/ring_scheduler.py | 18 +++---- src/shared/python/shared/three_cx_client.py | 54 ++++++++++++++++++--- src/slack-bot/app.py | 16 +++--- template.yaml | 20 ++++---- 6 files changed, 84 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index b96f377..6e5dc17 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # After-Hours Shift Manager -Slack bot for managing after-hours on-call shifts at Sea Haven Industries. Employees can pick up, drop, and swap shifts directly from Slack. Changes automatically update 3CX ring group routing via the integrated ring scheduler. +Slack bot for managing after-hours on-call shifts at Sea Haven Industries. Employees can pick up, drop, and swap shifts directly from Slack. Changes automatically update 3CX queue routing via the integrated ring scheduler. ## How It Works -A recurring weekly schedule assigns employees to after-hours phone duty. Weekend shifts are split into Day (8am-5pm) and Night (5pm-8am). Any unassigned shift shows as **Available** in Slack with a pickup button. When someone picks up or drops a shift for today, the 3CX ring group is updated immediately. Future changes take effect when the ring scheduler runs at 8am daily and 5pm on weekends. +A recurring weekly schedule assigns employees to after-hours phone duty. Weekend shifts are split into Day (8am-5pm) and Night (5pm-8am). Any unassigned shift shows as **Available** in Slack with a pickup button. When someone picks up or drops a shift for today, the 3CX queue is updated immediately. Future changes take effect when the ring scheduler runs at 8am daily and 5pm on weekends. The weekly schedule post is updated live when shifts change, and the previous week's post is automatically deleted when the new one goes out. @@ -46,7 +46,7 @@ Dates accept: `today`, `tomorrow`, `monday`-`sunday`, `4/5`, `2026-04-05` - **Data**: DynamoDB single-table (`afterhours-shifts`) - **IaC**: AWS SAM (`template.yaml`) with shared Lambda Layer - **Slack**: Slack Bolt framework with `/oncall` slash command -- **3CX Integration**: Ring group routing updated directly via 3CX RingGroup XAPI +- **3CX Integration**: Queue routing updated directly via 3CX Queue XAPI - **Secrets**: AWS Secrets Manager (`afterhours-shift-manager/*`) ### Lambda Functions @@ -56,7 +56,7 @@ Dates accept: `today`, `tomorrow`, `monday`-`sunday`, `4/5`, `2026-04-05` | `afterhours-shift-manager` | API Gateway (POST /slack/events) | Slack bot — handles `/oncall` commands and interactive buttons | | `afterhours-weekly-post` | EventBridge (Monday 7am ET) | Posts weekly schedule to Slack, sends pay report email | | `afterhours-roster-sync` | EventBridge (daily 6am ET) | Syncs employee roster from 3CX | -| `afterhours-ring-scheduler` | EventBridge (daily 8am ET + weekend 5pm ET) | Updates 3CX ring group routing based on who's on shift | +| `afterhours-ring-scheduler` | EventBridge (daily 8am ET + weekend 5pm ET) | Updates 3CX queue routing based on who's on shift | ### Project Layout @@ -65,7 +65,7 @@ src/ slack-bot/ Slack Bolt Lambda (handler + app) weekly-post/ Monday schedule + pay post roster-sync/ Daily 3CX roster sync - ring-scheduler/ 3CX ring group routing updates + ring-scheduler/ 3CX queue routing updates shared/ Lambda Layer (schedule, blocks, 3CX client, secrets) ``` diff --git a/src/ring-scheduler/app.py b/src/ring-scheduler/app.py index b05d592..4e3ce9a 100644 --- a/src/ring-scheduler/app.py +++ b/src/ring-scheduler/app.py @@ -1,4 +1,4 @@ -"""Lambda handler — updates 3CX ring group routing based on who's on shift. +"""Lambda handler — updates 3CX queue routing based on who's on shift. Triggered by EventBridge at 8am ET daily and 5pm ET on weekends. Also invoked directly by the Slack bot for same-day shift changes. @@ -10,7 +10,7 @@ import os from datetime import datetime from zoneinfo import ZoneInfo -from shared.ring_scheduler import update_ring_group +from shared.ring_scheduler import update_queue_routing from shared.schedule import FALLBACK_EXTENSION, ShiftSchedule from shared.secrets import get_secret @@ -32,7 +32,7 @@ def handler(event, context): now = datetime.now(EASTERN) current_hour = now.hour day_name = now.strftime("%A") - logger.info("Running ring group scheduler at %s", now.isoformat()) + logger.info("Running queue scheduler at %s", now.isoformat()) force = event.get("force", False) valid_hours = {8, 17} if day_name in WEEKEND_DAYS else {8} @@ -55,9 +55,9 @@ def handler(event, context): extension = ext if source != "available" else FALLBACK_EXTENSION secret_prefix = os.environ["TCX_SECRET_PREFIX"] - result = update_ring_group( + result = update_queue_routing( extension=extension, - ring_group_number=os.environ["RING_GROUP_NUMBER"], + queue_number=os.environ["QUEUE_NUMBER"], domain=get_secret(f"{secret_prefix}domain"), client_id=get_secret(f"{secret_prefix}client-id"), client_secret=get_secret(f"{secret_prefix}client-secret"), diff --git a/src/shared/python/shared/ring_scheduler.py b/src/shared/python/shared/ring_scheduler.py index cee3bd2..4c786e9 100644 --- a/src/shared/python/shared/ring_scheduler.py +++ b/src/shared/python/shared/ring_scheduler.py @@ -1,4 +1,4 @@ -"""Core ring group routing logic shared between the scheduled Lambda and the Slack bot.""" +"""Core queue routing logic shared between the scheduled Lambda and the Slack bot.""" import logging @@ -7,27 +7,27 @@ from shared.three_cx_client import ThreeCXClient logger = logging.getLogger(__name__) -def update_ring_group( +def update_queue_routing( extension: str, - ring_group_number: str, + queue_number: str, domain: str, client_id: str, client_secret: str, ) -> dict: - """Update 3CX ring group forwarding to route calls to the given extension.""" + """Update 3CX queue forwarding to route calls to the given extension.""" client = ThreeCXClient( domain=domain, auth_mode="oauth", client_id=client_id, client_secret=client_secret, ) - ring_group = client.get_ring_group(ring_group_number) - client.update_ring_group_forwarding( - ring_group_id=ring_group["Id"], + queue = client.get_queue(queue_number) + client.update_queue_forwarding( + queue_id=queue["Id"], closed_destination=extension, holiday_destination=extension, ) logger.info( - "Updated ring group %s to forward to Ext %s", ring_group_number, extension + "Updated queue %s to forward to Ext %s", queue_number, extension ) - return {"extension": extension, "ring_group": ring_group_number} + return {"extension": extension, "queue": queue_number} diff --git a/src/shared/python/shared/three_cx_client.py b/src/shared/python/shared/three_cx_client.py index 09b5c35..15c26ee 100644 --- a/src/shared/python/shared/three_cx_client.py +++ b/src/shared/python/shared/three_cx_client.py @@ -98,13 +98,7 @@ class ThreeCXClient: closed_destination: str, holiday_destination: str, ): - """Update the OutOfOfficeRoute and HolidaysRoute on a ring group. - - Args: - ring_group_id: Numeric ID from the ring group entity - closed_destination: Extension number for after-hours routing - holiday_destination: Extension number for holiday routing - """ + """Update the OutOfOfficeRoute and HolidaysRoute on a ring group.""" payload = { "OutOfOfficeRoute": { "IsPromptEnabled": False, @@ -135,3 +129,49 @@ class ThreeCXClient: holiday_destination, ) return resp.status_code + + def get_queue(self, extension_number: str) -> dict: + """Fetch queue config by extension number.""" + resp = self.session.get( + f"{self.base_url}/xapi/v1/Queues/Pbx.GetByNumber(number='{extension_number}')", + ) + resp.raise_for_status() + return resp.json() + + def update_queue_forwarding( + self, + queue_id: int, + closed_destination: str, + holiday_destination: str, + ): + """Update the OutOfOfficeRoute and HolidaysRoute on a queue.""" + payload = { + "OutOfOfficeRoute": { + "IsPromptEnabled": False, + "Route": { + "To": "Extension", + "Number": closed_destination, + "External": "", + }, + }, + "HolidaysRoute": { + "IsPromptEnabled": False, + "Route": { + "To": "Extension", + "Number": holiday_destination, + "External": "", + }, + }, + } + resp = self.session.patch( + f"{self.base_url}/xapi/v1/Queues({queue_id})", + json=payload, + ) + resp.raise_for_status() + logger.info( + "Updated queue %s: closed->Ext %s, holiday->Ext %s", + queue_id, + closed_destination, + holiday_destination, + ) + return resp.status_code diff --git a/src/slack-bot/app.py b/src/slack-bot/app.py index 8c1ffa7..583b920 100644 --- a/src/slack-bot/app.py +++ b/src/slack-bot/app.py @@ -15,7 +15,7 @@ from shared.blocks import ( build_shift_change_message, build_week_schedule, ) -from shared.ring_scheduler import update_ring_group +from shared.ring_scheduler import update_queue_routing from shared.schedule import FALLBACK_EXTENSION, ShiftSchedule from shared.secrets import get_secret @@ -66,22 +66,22 @@ def parse_date(text: str) -> datetime | None: def _update_3cx_routing(extension: str) -> None: - """Update the 3CX ring group to forward calls to the given extension.""" - ring_group_number = os.environ.get("RING_GROUP_NUMBER") + """Update the 3CX queue to forward calls to the given extension.""" + queue_number = os.environ.get("QUEUE_NUMBER") secret_prefix = os.environ.get("TCX_SECRET_PREFIX") - if not ring_group_number or not secret_prefix: - logger.warning("3CX env vars not set — skipping ring group update") + if not queue_number or not secret_prefix: + logger.warning("3CX env vars not set — skipping queue update") return try: - update_ring_group( + update_queue_routing( extension=extension, - ring_group_number=ring_group_number, + queue_number=queue_number, domain=get_secret(f"{secret_prefix}domain"), client_id=get_secret(f"{secret_prefix}client-id"), client_secret=get_secret(f"{secret_prefix}client-secret"), ) except Exception: - logger.exception("Failed to update 3CX ring group") + logger.exception("Failed to update 3CX queue") def is_today(date_str: str) -> bool: diff --git a/template.yaml b/template.yaml index 0d0ec56..617dc0f 100644 --- a/template.yaml +++ b/template.yaml @@ -9,10 +9,10 @@ Parameters: ShiftChannel: Type: String Description: Slack channel ID for schedule posts and shift notifications - RingGroupNumber: + QueueNumber: Type: String - Default: "800" - Description: 3CX ring group extension number to update + Default: "801" + Description: 3CX queue extension number to update Globals: Function: @@ -70,7 +70,7 @@ Resources: SLACK_SIGNING_SECRET: afterhours-shift-manager/slack-signing-secret SHIFT_CHANNEL: !Ref ShiftChannel TCX_SECRET_PREFIX: afterhours-shift-manager/3cx- - RING_GROUP_NUMBER: !Ref RingGroupNumber + QUEUE_NUMBER: !Ref QueueNumber TZ: !Ref Timezone Policies: - DynamoDBCrudPolicy: @@ -178,7 +178,7 @@ Resources: Description: "Sync roster from 3CX at 6am EDT" Enabled: true - # --- Ring Group Scheduler (daily 3CX routing updates) --- + # --- Ring Scheduler (daily 3CX queue routing updates) --- RingSchedulerFunction: Type: AWS::Serverless::Function Properties: @@ -192,7 +192,7 @@ Resources: Variables: SHIFT_TABLE: !Ref ShiftTable TCX_SECRET_PREFIX: afterhours-shift-manager/3cx- - RING_GROUP_NUMBER: !Ref RingGroupNumber + QUEUE_NUMBER: !Ref QueueNumber TZ: !Ref Timezone Policies: - Statement: @@ -212,26 +212,26 @@ Resources: Type: Schedule Properties: Schedule: cron(0 13 ? * * *) - Description: "Update 3CX ring group at 8am EST" + Description: "Update 3CX queue at 8am EST" Enabled: true DailyScheduleEDT: Type: Schedule Properties: Schedule: cron(0 12 ? * * *) - Description: "Update 3CX ring group at 8am EDT" + Description: "Update 3CX queue at 8am EDT" Enabled: true # Weekends at 5pm ET — switch to night shift person WeekendEveningEST: Type: Schedule Properties: Schedule: cron(0 22 ? * SAT,SUN *) - Description: "Update 3CX ring group at 5pm EST weekends" + Description: "Update 3CX queue at 5pm EST weekends" Enabled: true WeekendEveningEDT: Type: Schedule Properties: Schedule: cron(0 21 ? * SAT,SUN *) - Description: "Update 3CX ring group at 5pm EDT weekends" + Description: "Update 3CX queue at 5pm EDT weekends" Enabled: true # --- CloudWatch Log Groups (explicit 60-day retention) ---