Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/connect order to Freshdesk ticket #3620

Draft
wants to merge 12 commits into
base: feat/add-freshdesk-properties
Choose a base branch
from
25 changes: 22 additions & 3 deletions cg/clients/freshdesk/freshdesk_client.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import logging
from http import HTTPStatus
from pathlib import Path
from typing import List, Tuple, Union
from typing import List

from requests import Response, Session
from requests import Session
from requests.adapters import HTTPAdapter
from urllib3 import Retry

Expand All @@ -18,7 +18,7 @@
class FreshdeskClient:
"""Client for communicating with the freshdesk REST API."""

def __init__(self, base_url: str, api_key: str, order_email_id: int):
def __init__(self, base_url: str, api_key: str):
self.base_url = base_url
self.api_key = api_key
self.session = self._get_session()
Expand Down Expand Up @@ -58,3 +58,22 @@ def _configure_retries(session: Session) -> None:
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("https://", adapter)

@handle_client_errors
def reply_to_ticket(self, ticket_id: str, reply: dict, attachments: List[Path] = None) -> None:
"""Send a reply to an existing ticket in Freshdesk."""
url = f"{self.base_url}{EndPoints.TICKETS}/{ticket_id}/reply"

files = prepare_attachments(attachments) if attachments else None

response = self.session.post(url=url, data={"body": reply["body"]}, files=files)
if response.status_code == HTTPStatus.OK:
LOG.info("Successfully replied to ticket %s", ticket_id)
else:
LOG.error(
"Failed to reply to ticket %s. Status code: %s, Response: %s",
ticket_id,
response.status_code,
response.text,
)
response.raise_for_status()
1 change: 0 additions & 1 deletion cg/clients/freshdesk/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ class TicketCreate(BaseModel):
"""Freshdesk ticket."""

attachments: List[Union[str, bytes]] = Field(default_factory=list)
email: str
email_config_id: int | None = None
description: str
name: str
Expand Down
3 changes: 1 addition & 2 deletions cg/meta/orders/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,12 @@ def submit(self, project: OrderType, order_in: OrderIn, user_name: str, user_mai
ticket_number: str | None = self.ticket_handler.parse_ticket_number(order_in.name)
if not ticket_number:
ticket_number = self.ticket_handler.create_ticket(
order=order_in, user_name=user_name, user_mail=user_mail, project=project
order=order_in, user_name=user_name, project=project
)
else:
self.ticket_handler.connect_to_ticket(
order=order_in,
user_name=user_name,
user_mail=user_mail,
project=project,
ticket_number=ticket_number,
)
Expand Down
47 changes: 21 additions & 26 deletions cg/meta/orders/ticket_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@
from tempfile import TemporaryDirectory
from typing import Any

from sendmail_container import FormDataRequest

from cg.clients.freshdesk.freshdesk_client import FreshdeskClient
from cg.clients.freshdesk.models import TicketCreate, TicketResponse
from cg.clients.freshdesk.utils import create_temp_attachment_file
from cg.models.orders.order import OrderIn
from cg.models.orders.samples import Of1508Sample
from cg.store.models import Customer, Sample
Expand All @@ -22,26 +19,25 @@ class TicketHandler:

NEW_LINE = "<br />"

def __init__(self, status_db: Store, client: FreshdeskClient, env: str):
def __init__(self, status_db: Store, client: FreshdeskClient, system_email_id: int, env: str):
self.client: FreshdeskClient = client
self.status_db: Store = status_db
self.system_email_id: int = system_email_id
self.env = env

@staticmethod
def parse_ticket_number(name: str) -> str | None:
"""Try to parse a ticket number from a string"""
# detect manual ticket assignment
ticket_match = re.fullmatch(r"#([0-9]{6})", name)
ticket_match = re.fullmatch(r"#([0-9]{6,10})", name)
if ticket_match:
ticket_id = ticket_match.group(1)
LOG.info(f"{ticket_id}: detected ticket in order name")
return ticket_id
LOG.info(f"Could not detected ticket number in name {name}")
return None

def create_ticket(
self, order: OrderIn, user_name: str, user_mail: str, project: str
) -> int | None:
def create_ticket(self, order: OrderIn, user_name: str, project: str) -> int | None:
"""Create a ticket and return the ticket number"""
message: str = self.create_new_ticket_header(
message=self.create_xml_sample_list(order=order, user_name=user_name),
Expand All @@ -53,8 +49,8 @@ def create_ticket(
attachments: Path = self.create_attachment_file(order=order, temp_dir=temp_dir)

freshdesk_ticket = TicketCreate(
email=user_mail,
description=message,
email_config_id=self.system_email_id,
name=user_name,
subject=order.name,
type="Order",
Expand Down Expand Up @@ -193,27 +189,26 @@ def replace_empty_string_with_none(cls, obj: Any) -> Any:
return obj

def connect_to_ticket(
self, order: OrderIn, user_name: str, user_mail: str, project: str, ticket_number: str
self, order: OrderIn, user_name: str, project: str, ticket_number: str
) -> None:
"""Appends a new order message to the ticket selected by the customer"""
LOG.info(f"Connecting order to ticket {ticket_number}")
LOG.info("Connecting order to ticket %s", ticket_number)

message: str = self.add_existing_ticket_header(
message=self.create_xml_sample_list(order=order, user_name=user_name),
order=order,
project=project,
)
sender_prefix, email_server_alias = user_mail.split("@")
temp_dir: TemporaryDirectory = create_temp_attachment_file(
content=self.replace_empty_string_with_none(obj=order.dict()), file_name="order.json"
)
email_form = FormDataRequest(
sender_prefix=sender_prefix,
email_server_alias=email_server_alias,
request_uri=self.client.base_url,
recipients=user_mail,
mail_title=f"[#{ticket_number}]",
mail_body=message,
attachments=[Path(f"{temp_dir.name}/order.json")],
)
email_form.submit()
temp_dir.cleanup()

with TemporaryDirectory() as temp_dir:
attachments: Path = self.create_attachment_file(order=order, temp_dir=temp_dir)

reply_payload = {"body": message, "attachments": []}

LOG.info("Reply payload: %s", reply_payload)

self.client.reply_to_ticket(
ticket_id=ticket_number, reply=reply_payload, attachments=[attachments]
)

LOG.info("Connected order to ticket %s in Freshdesk", ticket_number)
5 changes: 2 additions & 3 deletions cg/server/ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,12 @@ def init_app(self, app):
order_service = OrderService(store=db, status_service=summary_service)
sample_service = SampleService(db)
freshdesk_client = FreshdeskClient(
base_url=app_config.freshdesk_url,
api_key=app_config.freshdesk_api_key,
order_email_id=app_config.freshdesk_order_email_id,
base_url=app_config.freshdesk_url, api_key=app_config.freshdesk_api_key
)
ticket_handler = TicketHandler(
client=freshdesk_client,
status_db=db,
system_email_id=app_config.freshdesk_order_email_id,
env=app_config.freshdesk_environment,
)
orders_api = OrdersAPI(
Expand Down
4 changes: 1 addition & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -650,9 +650,7 @@ def osticket(ticket_id: str) -> MockOsTicket:
@pytest.fixture
def freshdesk_client() -> FreshdeskClient:
"""Return a FreshdeskClient instance with mock parameters."""
client = FreshdeskClient(
base_url="https://mock.freshdesk.com", api_key="mock_api_key", order_email_id=2024
)
client = FreshdeskClient(base_url="https://mock.freshdesk.com", api_key="mock_api_key")
return client


Expand Down
6 changes: 2 additions & 4 deletions tests/meta/orders/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,7 @@ def tomte_status_data(tomte_order_to_submit: dict):

@pytest.fixture
def freshdesk_client():
return FreshdeskClient(
base_url="https://example.com", api_key="dummy_api_key", order_email_id=12345
)
return FreshdeskClient(base_url="https://example.com", api_key="dummy_api_key")


@pytest.fixture(scope="function")
Expand All @@ -144,4 +142,4 @@ def orders_api(base_store: Store, lims_api: MockLimsAPI, ticket_handler: TicketH

@pytest.fixture
def ticket_handler(store: Store, freshdesk_client: FreshdeskClient):
return TicketHandler(status_db=store, client=freshdesk_client, env="test")
return TicketHandler(status_db=store, client=freshdesk_client, system_email_id=1, env="test")
63 changes: 43 additions & 20 deletions tests/meta/orders/test_meta_orders_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def monkeypatch_process_lims(monkeypatch: pytest.MonkeyPatch, order_data: OrderI
)


def mock_freshdesk_ticket_creation(mock_create_ticket, ticket_id: str, user_mail: str):
def mock_freshdesk_ticket_creation(mock_create_ticket, ticket_id: str):
"""Helper function to mock Freshdesk ticket creation."""
mock_create_ticket.return_value = TicketResponse(
id=int(ticket_id),
Expand All @@ -59,6 +59,11 @@ def mock_freshdesk_ticket_creation(mock_create_ticket, ticket_id: str, user_mail
)


def mock_freshdesk_reply_to_ticket(mock_reply_to_ticket):
"""Helper function to mock Freshdesk reply to ticket."""
mock_reply_to_ticket.return_value = None


@pytest.mark.parametrize(
"order_type",
[
Expand All @@ -84,10 +89,13 @@ def test_submit(
user_mail: str,
user_name: str,
):
with patch("cg.meta.orders.ticket_handler.FormDataRequest.submit", return_value=None), patch(
with patch(
"cg.clients.freshdesk.freshdesk_client.FreshdeskClient.create_ticket"
) as mock_create_ticket:
mock_freshdesk_ticket_creation(mock_create_ticket, ticket_id, user_mail)
) as mock_create_ticket, patch(
"cg.clients.freshdesk.freshdesk_client.FreshdeskClient.reply_to_ticket"
) as mock_reply_to_ticket:
mock_freshdesk_ticket_creation(mock_create_ticket, ticket_id)
mock_freshdesk_reply_to_ticket(mock_reply_to_ticket)

order_data = OrderIn.parse_obj(obj=all_orders_to_submit[order_type], project=order_type)
monkeypatch_process_lims(monkeypatch, order_data)
Expand Down Expand Up @@ -198,10 +206,16 @@ def test_submit_scout_legal_sample_customer(
sample_store: Store,
user_mail: str,
user_name: str,
ticket_id: str,
):
with patch(
"cg.meta.orders.ticket_handler.FormDataRequest.submit", return_value=None
) as mail_patch:
"cg.clients.freshdesk.freshdesk_client.FreshdeskClient.create_ticket"
) as mock_create_ticket, patch(
"cg.clients.freshdesk.freshdesk_client.FreshdeskClient.reply_to_ticket"
) as mock_reply_to_ticket:
mock_freshdesk_ticket_creation(mock_create_ticket, ticket_id)
mock_freshdesk_reply_to_ticket(mock_reply_to_ticket)

order_data = OrderIn.parse_obj(obj=all_orders_to_submit[order_type], project=order_type)
monkeypatch_process_lims(monkeypatch, order_data)

Expand Down Expand Up @@ -299,10 +313,16 @@ def test_submit_fluffy_duplicate_sample_case_name(
orders_api: OrdersAPI,
user_mail: str,
user_name: str,
ticket_id: str,
):
with patch(
"cg.meta.orders.ticket_handler.FormDataRequest.submit", return_value=None
) as mail_patch:
"cg.clients.freshdesk.freshdesk_client.FreshdeskClient.create_ticket"
) as mock_create_ticket, patch(
"cg.clients.freshdesk.freshdesk_client.FreshdeskClient.reply_to_ticket"
) as mock_reply_to_ticket:
mock_freshdesk_ticket_creation(mock_create_ticket, ticket_id)
mock_freshdesk_reply_to_ticket(mock_reply_to_ticket)

# GIVEN we have an order with a case that is already in the database
order_data = OrderIn.parse_obj(obj=all_orders_to_submit[order_type], project=order_type)
monkeypatch_process_lims(monkeypatch, order_data)
Expand Down Expand Up @@ -331,11 +351,12 @@ def test_submit_unique_sample_case_name(
ticket_id: str,
):
with patch(
"cg.meta.orders.ticket_handler.FormDataRequest.submit", return_value=None
) as mail_patch, patch(
"cg.clients.freshdesk.freshdesk_client.FreshdeskClient.create_ticket"
) as mock_create_ticket:
mock_freshdesk_ticket_creation(mock_create_ticket, ticket_id, user_mail)
) as mock_create_ticket, patch(
"cg.clients.freshdesk.freshdesk_client.FreshdeskClient.reply_to_ticket"
) as mock_reply_to_ticket:
mock_freshdesk_ticket_creation(mock_create_ticket, ticket_id)
mock_freshdesk_reply_to_ticket(mock_reply_to_ticket)

# GIVEN we have an order with a case that is not existing in the database
order_data = OrderIn.parse_obj(obj=mip_order_to_submit, project=OrderType.MIP_DNA)
Expand Down Expand Up @@ -512,11 +533,12 @@ def test_submit_unique_sample_name(
user_name: str,
):
with patch(
"cg.meta.orders.ticket_handler.FormDataRequest.submit", return_value=None
) as mail_patch, patch(
"cg.clients.freshdesk.freshdesk_client.FreshdeskClient.create_ticket"
) as mock_create_ticket:
mock_freshdesk_ticket_creation(mock_create_ticket, ticket_id, user_mail)
) as mock_create_ticket, patch(
"cg.clients.freshdesk.freshdesk_client.FreshdeskClient.reply_to_ticket"
) as mock_reply_to_ticket:
mock_freshdesk_ticket_creation(mock_create_ticket, ticket_id)
mock_freshdesk_reply_to_ticket(mock_reply_to_ticket)

# GIVEN we have an order with a sample that is not existing in the database
order_data = OrderIn.parse_obj(obj=all_orders_to_submit[order_type], project=order_type)
Expand Down Expand Up @@ -599,11 +621,12 @@ def test_not_sarscov2_submit_duplicate_sample_name(
user_name: str,
):
with patch(
"cg.meta.orders.ticket_handler.FormDataRequest.submit", return_value=None
) as mail_patch, patch(
"cg.clients.freshdesk.freshdesk_client.FreshdeskClient.create_ticket"
) as mock_create_ticket:
mock_freshdesk_ticket_creation(mock_create_ticket, ticket_id, user_mail)
) as mock_create_ticket, patch(
"cg.clients.freshdesk.freshdesk_client.FreshdeskClient.reply_to_ticket"
) as mock_reply_to_ticket:
mock_freshdesk_ticket_creation(mock_create_ticket, ticket_id)
mock_freshdesk_reply_to_ticket(mock_reply_to_ticket)

# GIVEN we have an order with samples that are already in the database
order_data = OrderIn.parse_obj(obj=all_orders_to_submit[order_type], project=order_type)
Expand Down
Loading