Deduplicate 3CX forwarding payload and add shift type to pick command

Extract _update_forwarding helper in ThreeCXClient to share the
payload between queue and ring group methods. Add optional day/night
argument to /oncall pick so users can target a specific weekend shift.
This commit is contained in:
Adam Moussa 2026-05-12 19:22:11 -04:00
parent 0d119f494b
commit c3b35ed62e
3 changed files with 49 additions and 81 deletions

View file

@ -24,8 +24,8 @@ def update_queue_routing(
queue = client.get_queue(queue_number)
client.update_queue_forwarding(
queue_id=queue["Id"],
closed_destination=extension,
holiday_destination=extension,
closed=extension,
holiday=extension,
)
logger.info("Updated queue %s to forward to Ext %s", queue_number, extension)
return {"extension": extension, "queue": queue_number}

View file

@ -84,6 +84,33 @@ class ThreeCXClient:
resp.raise_for_status()
return resp.json().get("value", [])
def _update_forwarding(
self, resource: str, resource_id: int, closed: str, holiday: str
):
payload = {
"OutOfOfficeRoute": {
"IsPromptEnabled": False,
"Route": {"To": "Extension", "Number": closed, "External": ""},
},
"HolidaysRoute": {
"IsPromptEnabled": False,
"Route": {"To": "Extension", "Number": holiday, "External": ""},
},
}
resp = self.session.patch(
f"{self.base_url}/xapi/v1/{resource}({resource_id})",
json=payload,
)
resp.raise_for_status()
logger.info(
"Updated %s %s: closed->Ext %s, holiday->Ext %s",
resource,
resource_id,
closed,
holiday,
)
return resp.status_code
def get_ring_group(self, extension_number: str) -> dict:
"""Fetch ring group config by extension number."""
resp = self.session.get(
@ -93,42 +120,9 @@ class ThreeCXClient:
return resp.json()
def update_ring_group_forwarding(
self,
ring_group_id: int,
closed_destination: str,
holiday_destination: str,
self, ring_group_id: int, closed: str, holiday: str
):
"""Update the OutOfOfficeRoute and HolidaysRoute on a ring group."""
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/RingGroups({ring_group_id})",
json=payload,
)
resp.raise_for_status()
logger.info(
"Updated ring group %s: closed->Ext %s, holiday->Ext %s",
ring_group_id,
closed_destination,
holiday_destination,
)
return resp.status_code
return self._update_forwarding("RingGroups", ring_group_id, closed, holiday)
def get_queue(self, extension_number: str) -> dict:
"""Fetch queue config by extension number."""
@ -138,40 +132,5 @@ class ThreeCXClient:
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
def update_queue_forwarding(self, queue_id: int, closed: str, holiday: str):
return self._update_forwarding("Queues", queue_id, closed, holiday)

View file

@ -377,9 +377,11 @@ def create_app(
respond(text=f"Linked your account to *{employee['name']}* (Ext {ext}).")
def _handle_pick(respond, schedule, user_id, text, channel_id, client):
parts = text.split(maxsplit=1)
parts = text.split()
if len(parts) < 2:
respond(text="Usage: `/oncall pick <date>` (e.g. `/oncall pick friday`)")
respond(
text="Usage: `/oncall pick <date> [day|night]` (e.g. `/oncall pick friday`)"
)
return
employee = schedule.get_employee_by_slack_id(user_id)
@ -389,10 +391,14 @@ def create_app(
)
return
date = parse_date(parts[1])
explicit_shift = (
parts[2] if len(parts) > 2 and parts[2] in ("day", "night") else None
)
date_text = parts[1]
date = parse_date(date_text)
if not date:
respond(
text=f"Couldn't parse date: `{parts[1]}`. Try: today, tomorrow, friday, 4/5, 2026-04-05"
text=f"Couldn't parse date: `{date_text}`. Try: today, tomorrow, friday, 4/5, 2026-04-05"
)
return
@ -403,9 +409,11 @@ def create_app(
day_name = date.strftime("%A")
# On weekends, find the first available shift (day then night)
shift_type = "night"
if day_name in WEEKEND_DAYS:
if explicit_shift:
shift_type = explicit_shift
ext, name, source = schedule.resolve_shift(date_str, day_name, shift_type)
elif day_name in WEEKEND_DAYS:
shift_type = "night"
for st in ("day", "night"):
ext, name, source = schedule.resolve_shift(date_str, day_name, st)
if source == "available":
@ -414,6 +422,7 @@ def create_app(
else:
ext, name, source = schedule.resolve_shift(date_str, day_name)
else:
shift_type = "night"
ext, name, source = schedule.resolve_shift(date_str, day_name)
# Already assigned to someone else (not open)