Skip to content

Commit 7b4a88e

Browse files
committed
CU-861mv9d7e: introduce phone_directory contacts selecting mode
1 parent f302887 commit 7b4a88e

File tree

6 files changed

+136
-26
lines changed

6 files changed

+136
-26
lines changed

app/bss/adapters/portaswitch/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ ENV PORTASWITCH_CONTACTS_SELECTING="<contacts-selecting>"
6666
# By default does not apply filter by type
6767
ENV PORTASWITCH_CONTACTS_SELECTING_EXTENSION_TYPES="Account;Group;Unassigned"
6868

69-
# Defines a list of customer ids for PORTASWITCH_CONTACTS_SELECTING=phonebook mode (optional)
70-
ENV PORTASWITCH_CONTACTS_SELECTING_PHONEBOOK_CUSTOMER_IDS="8;10;11;13;14"
69+
# Defines a list of customer ids for PORTASWITCH_CONTACTS_SELECTING=phonebook|phone_directory modes (optional)
70+
ENV PORTASWITCH_CONTACTS_SELECTING_CUSTOMER_IDS="8;10;11;13;14"
7171

7272
# Ensures that the contacts API filters accounts that do not have an `extension_id` (optional):
7373
ENV PORTASWITCH_CONTACTS_SKIP_WITHOUT_EXTENSION="True"

app/bss/adapters/portaswitch/adapter.py

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -425,27 +425,10 @@ def retrieve_contacts(self, session: SessionInfo, user: UserInfo) -> list[Contac
425425
contacts.append(Serializer.get_contact_info_by_account(account, i_account))
426426
case PortaSwitchContactsSelectingMode.PHONEBOOK:
427427
phonebook = self._account_api.get_phonebook_list(access_token, 1, 100)['phonebook_rec_list']
428-
number_to_accounts = {}
429-
# Retrieve accounts for a pre-defined list of customers to map them to phonebook records
430-
with ThreadPoolExecutor(max_workers=10) as executor:
431-
future_to_customer_id = {
432-
executor.submit(lambda cid: self._admin_api.get_account_list(int(cid))["account_list"],
433-
cid): cid
434-
for cid in self._portaswitch_settings.CONTACTS_SELECTING_PHONEBOOK_CUSTOMER_IDS
435-
}
436-
437-
for future in as_completed(future_to_customer_id):
438-
try:
439-
accounts = future.result()
440-
for account in accounts:
441-
number_to_accounts[account["id"]] = account
442-
except Exception as e:
443-
logging.warning(
444-
f"Error fetching accounts for customer {future_to_customer_id[future]}: {e}")
445-
446-
contacts = []
428+
number_to_accounts = self._get_number_to_customer_accounts_map()
429+
447430
for record in phonebook:
448-
# Normalize phone number by removing '+' prefix
431+
# Normalize phone number by removing the '+' prefix
449432
phonebook_record_number = record.get("phone_number").replace("+", "")
450433
phonebook_contact_info = Serializer.get_contact_info_by_phonebook_record(record)
451434

@@ -463,6 +446,40 @@ def retrieve_contacts(self, session: SessionInfo, user: UserInfo) -> list[Contac
463446
if contact.is_current_user is not True:
464447
contacts.append(contact)
465448

449+
case PortaSwitchContactsSelectingMode.PHONE_DIRECTORY:
450+
phone_directories = self._account_api.get_phone_directory_list(access_token, 1, 100)[
451+
'phone_directory_list']
452+
number_to_accounts = self._get_number_to_customer_accounts_map()
453+
454+
for directory in phone_directories:
455+
directory_info = self._account_api.get_phone_directory_info(
456+
access_token,
457+
directory['i_ua_config_directory'],
458+
1,
459+
10_000
460+
)['phone_directory_info']
461+
for record in directory_info['directory_records']:
462+
# Normalize phone number by removing the '+' prefix
463+
phone_directory_record_number = record.get("office_number").replace("+", "")
464+
phone_directory_contact_info = Serializer.get_contact_info_by_phone_directory_record(record,
465+
directory_info[
466+
'name'])
467+
468+
if account := number_to_accounts.get(phone_directory_record_number):
469+
# If we found a matching account, use its contact info but update with phone directory data
470+
contact = Serializer.get_contact_info_by_account(account, i_account)
471+
contact.first_name = phone_directory_contact_info.first_name
472+
contact.last_name = phone_directory_contact_info.last_name
473+
contact.numbers.main = phone_directory_record_number
474+
else:
475+
# No matching account found, use phone directory contact info as is
476+
contact = phone_directory_contact_info
477+
if contact.numbers.main:
478+
contact.numbers.main = contact.numbers.main.replace("+", "")
479+
480+
if contact.is_current_user is not True:
481+
contacts.append(contact)
482+
466483
# Extend the contact list with custom entries
467484
contacts.extend([Serializer.get_contact_info_by_custom_entry(entry) for entry in
468485
self._portaswitch_settings.CONTACTS_CUSTOM])
@@ -806,3 +823,25 @@ def _check_allowed_addons(self, account_info: dict):
806823

807824
if not allowed_addons.intersection(assigned_addon_names):
808825
raise WebTritErrorException(403, "Access denied: required add-on not assigned", code="addon_required")
826+
827+
def _get_number_to_customer_accounts_map(self) -> dict[str, dict]:
828+
"""Return a mapping of phone numbers to customer accounts."""
829+
number_to_accounts = {}
830+
# Retrieve accounts for a pre-defined list of customers to map them to phonebook records
831+
with ThreadPoolExecutor(max_workers=10) as executor:
832+
future_to_customer_id = {
833+
executor.submit(lambda cid: self._admin_api.get_account_list(int(cid))["account_list"],
834+
cid): cid
835+
for cid in self._portaswitch_settings.CONTACTS_SELECTING_CUSTOMER_IDS
836+
}
837+
838+
for future in as_completed(future_to_customer_id):
839+
try:
840+
accounts = future.result()
841+
for account in accounts:
842+
number_to_accounts[account["id"]] = account
843+
except Exception as e:
844+
logging.warning(
845+
f"Error fetching accounts for customer {future_to_customer_id[future]}: {e}")
846+
847+
return number_to_accounts

app/bss/adapters/portaswitch/api/account.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,51 @@ def get_phonebook_list(self, access_token: str, page: int, items_per_page: int)
263263
},
264264
access_token=access_token)
265265

266+
def get_phone_directory_list(self, access_token: str, page: int, items_per_page: int) -> dict:
267+
"""Return the phone directory list of the account.
268+
269+
Args:
270+
access_token (str): Token that authenticates the API user in the PortaBilling API using the account realm.
271+
page (int): Shows what page of the phone directory to return.
272+
items_per_page (int): Shows the number of items to return.
273+
274+
Returns:
275+
dict: API method execution result containing phone directory items.
276+
"""
277+
return self.__send_request(
278+
module="UA",
279+
method="get_phone_directory_list",
280+
params={
281+
"get_total": 1,
282+
"limit": items_per_page,
283+
"offset": items_per_page * (page - 1),
284+
},
285+
access_token=access_token)
286+
287+
def get_phone_directory_info(self, access_token: str, i_ua_config_directory: str, page: int, items_per_page: int) -> dict:
288+
"""Return the phone directory info of the account.
289+
290+
Args:
291+
access_token (str): Token that authenticates the API user in the PortaBilling API using the account realm.
292+
i_ua_config_directory (str): Identifier of the phone directory.
293+
page (int): Shows what page of the phone directory items to return.
294+
items_per_page (int): Shows the number of items to return.
295+
296+
Returns:
297+
dict: API method execution result containing phone directory items.
298+
"""
299+
return self.__send_request(
300+
module="UA",
301+
method="get_phone_directory_info",
302+
params={
303+
"i_ua_config_directory": i_ua_config_directory,
304+
"get_total": 1,
305+
"get_records": "Y",
306+
"limit": items_per_page,
307+
"offset": items_per_page * (page - 1),
308+
},
309+
access_token=access_token)
310+
266311
def get_call_recording(self, recording_id: int, access_token: str) -> tuple[str, Iterator]:
267312
"""Returns the bytes of the call recording file.
268313

app/bss/adapters/portaswitch/config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class PortaSwitchSettings(BaseSettings):
2222
SIGNIN_CREDENTIALS: PortaSwitchSignInCredentialsType = PortaSwitchSignInCredentialsType.SELF_CARE
2323
CONTACTS_SELECTING: PortaSwitchContactsSelectingMode = PortaSwitchContactsSelectingMode.ACCOUNTS
2424
CONTACTS_SELECTING_EXTENSION_TYPES: Union[List[PortaSwitchExtensionType], str] = list(PortaSwitchExtensionType)
25-
CONTACTS_SELECTING_PHONEBOOK_CUSTOMER_IDS: Union[List[str], str] = []
25+
CONTACTS_SELECTING_CUSTOMER_IDS: Union[List[str], str] = []
2626
CONTACTS_SKIP_WITHOUT_EXTENSION: bool = False
2727
CONTACTS_CUSTOM: Union[List[dict], str] = []
2828
HIDE_BALANCE_IN_USER_INFO: Optional[bool] = False
@@ -35,9 +35,9 @@ def decode_contacts_selecting_extension_types(cls, v: Union[List, str]) -> List[
3535
v = str(v)
3636
return [PortaSwitchExtensionType(x) for x in v.split(';')] if v else v
3737

38-
@field_validator("CONTACTS_SELECTING_PHONEBOOK_CUSTOMER_IDS", mode='before')
38+
@field_validator("CONTACTS_SELECTING_CUSTOMER_IDS", mode='before')
3939
@classmethod
40-
def decode_contacts_selecting_phonebook_customer_ids(cls, v: Union[List, str]) -> List[str]:
40+
def decode_contacts_selecting_customer_ids(cls, v: Union[List, str]) -> List[str]:
4141
v = str(v)
4242
return [x.strip() for x in v.split(';')] if v else v
4343

app/bss/adapters/portaswitch/serializer.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,31 @@ def get_contact_info_by_phonebook_record(phonebook_record_info: dict) -> Contact
162162
),
163163
)
164164

165+
@staticmethod
166+
def get_contact_info_by_phone_directory_record(phone_directory_record_info: dict, company_name: str) -> ContactInfo:
167+
"""Forms ContactInfo based on the input phonebook_record.
168+
Parameters:
169+
phone_directory_record_info: dict: The information about the phone directory record to be added to ContactInfo.
170+
company_name: str: The name of the phone directory.
171+
172+
Returns:
173+
ContactInfo: The filled structure of ContactInfo.
174+
"""
175+
additional_numbers = [
176+
phone_directory_record_info.get("mobile_number"),
177+
phone_directory_record_info.get("other_number"),
178+
]
179+
180+
return ContactInfo(
181+
company_name=company_name,
182+
first_name=phone_directory_record_info.get("first_name"),
183+
last_name=phone_directory_record_info.get("last_name"),
184+
numbers=Numbers(
185+
main=phone_directory_record_info.get("office_number"),
186+
additional=list(filter(lambda x: x not in (None, ""), additional_numbers))
187+
),
188+
)
189+
165190
@staticmethod
166191
def get_contact_info_by_custom_entry(custom_entry: dict) -> ContactInfo:
167192
"""Forms ContactInfo based on the input custom_entry.
@@ -336,7 +361,7 @@ def _xdr_to_call_status(failed: bool, disconnect_cause: int) -> str:
336361
return "accepted"
337362
else:
338363
return "error"
339-
364+
340365
@staticmethod
341366
def _call_recording_exist(cdr) -> bool:
342367
bit_flags = cdr["bit_flags"]

app/bss/adapters/portaswitch/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class PortaSwitchContactsSelectingMode(Enum):
2121
ACCOUNTS = "accounts"
2222
EXTENSIONS = "extensions"
2323
PHONEBOOK = "phonebook"
24+
PHONE_DIRECTORY = "phone_directory"
2425

2526
@classmethod
2627
def _missing_(cls, value):

0 commit comments

Comments
 (0)