Skip to content

Commit

Permalink
Send friendly messages if cant acquire counteragent (#28)
Browse files Browse the repository at this point in the history
* Diadoc client: add code and message to diadoc http exception

* Diadoc client:
1. Migrate to updated APIs
2. Capture acquire ca errors differently:
    - 409 errors as expected ones as simple messages
    - the left ones as exceptions

* Update TinkoffToDiadoc service to support updated diadoc client
  • Loading branch information
nkiryanov authored Jan 11, 2025
1 parent 0f8cc99 commit 82f99e0
Show file tree
Hide file tree
Showing 18 changed files with 165 additions and 84 deletions.
39 changes: 23 additions & 16 deletions src/diadoc/client.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
from sentry_sdk import capture_exception
import sentry_sdk

from diadoc import proto_types
from diadoc.exceptions import DiadocHTTPException
from diadoc.http import DiadocHTTP
from diadoc.models import DiadocPartner
from diadoc.types import DiadocCounteragent
from diadoc.types import DiadocId
from diadoc.types import DiadocOrganization


class DiadocClient:
def __init__(self) -> None:
self.http = DiadocHTTP()

def get_my_organizations(self) -> list[DiadocPartner]:
organizations: list[DiadocOrganization] = self.http.get("GetMyOrganizations")["Organizations"] # type: ignore
organizations: list[proto_types.Organization] = self.http.get("GetMyOrganizations")["Organizations"] # type: ignore
return DiadocPartner.from_organization_list(organizations)

def get_counteragents(self, my_diadoc_id: DiadocId) -> list[DiadocPartner]:
def get_counteragents(self, my_organization: DiadocPartner) -> list[DiadocPartner]:
"""Return organization counteragents from possibly paginated API.
To get next page:
Step 1: Get `IndexKey` from last counteragent in response
Step 2: Send request with query param `afterIndexKey` equals `IndexKey` from Step 1
"""
params = {"myOrgId": my_diadoc_id}
params = {"myBoxId": my_organization.diadoc_box_id}

counteragents: list[DiadocCounteragent] = []
counteragents: list[proto_types.Counteragent] = []
paginated_counteragents_empty = False

while not paginated_counteragents_empty:
paginated_counteragents: list[DiadocCounteragent] = self.http.get(url="V2/GetCounteragents", params=params)["Counteragents"] # type: ignore
paginated_counteragents: list[proto_types.Counteragent] = self.http.get(url="V3/GetCounteragents", params=params)["Counteragents"] # type: ignore

if paginated_counteragents:
counteragents += paginated_counteragents
Expand All @@ -44,17 +42,26 @@ def get_organizations_by_inn_kpp(self, inn: str, kpp: str | None = None) -> list
if kpp:
params["kpp"] = kpp

organizations: list[DiadocOrganization] = self.http.get("GetOrganizationsByInnKpp", params=params)["Organizations"] # type: ignore
organizations: list[proto_types.Organization] = self.http.get("GetOrganizationsByInnKpp", params=params)["Organizations"] # type: ignore
return DiadocPartner.from_organization_list(organizations)

def acquire_counteragent(self, my_diadoc_id: DiadocId, diadoc_id: DiadocId, message: str | None = None) -> None:
params = {"myOrgId": my_diadoc_id}
payload = {"OrgId": diadoc_id}
def acquire_counteragent(self, my_organization: DiadocPartner, to_acquire: DiadocPartner, message: str | None = None) -> None:
params = {"myBoxId": my_organization.diadoc_box_id}
payload = {"BoxId": to_acquire.diadoc_box_id}

if message:
payload["MessageToCounteragent"] = message

try:
self.http.post("V2/AcquireCounteragent", params=params, payload=payload)
except DiadocHTTPException as exception: # do not fail on HTTP errors, just send them to sentry
capture_exception(exception)
self.http.post("V3/AcquireCounteragent", params=params, payload=payload)
except DiadocHTTPException as exc: # do not fail on HTTP errors, just send them to sentry
if exc.code == 409:
sentry_sdk.capture_message(
message=(
"Can't acquire the counteragent. The reason is most likely that there is no roaming with the agent's provider. "
f"counteragent={to_acquire}, "
f"message={exc.message}"
)
)
else:
sentry_sdk.capture_exception(exc)
5 changes: 4 additions & 1 deletion src/diadoc/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ class DiadocException(Exception):


class DiadocHTTPException(DiadocException):
pass
def __init__(self, code: int, message: str):
super().__init__(code, message)
self.code = code
self.message = message
10 changes: 8 additions & 2 deletions src/diadoc/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,16 @@ def get_base_headers(self) -> dict[str, str]:

def raise_if_error_occurred(self, response: httpx.Response, expected_status_code: int) -> None:
if response.status_code != expected_status_code:
raise DiadocHTTPException(f"HTTP Error {response.status_code}, fetched url '{response.url}', {response.text}")
raise DiadocHTTPException(
code=response.status_code,
message=f"HTTP Error {response.status_code}, fetched url '{response.url}', {response.text}",
)

def get_response_json(self, response: httpx.Response) -> SimpleJSON:
try:
return response.json()
except json.JSONDecodeError as decode_error:
raise DiadocHTTPException(f"JSON decode error {decode_error}, fetched url '{response.url}, {response.text}'")
raise DiadocHTTPException(
code=response.status_code,
message=f"JSON decode error {decode_error}, fetched url '{response.url}, {response.text}'",
)
27 changes: 15 additions & 12 deletions src/diadoc/models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from dataclasses import dataclass
from dataclasses import field
from enum import Enum
from typing import Self

from app.models import LegalEntity
from diadoc.types import DiadocCounteragent
from diadoc.types import DiadocOrganization
from diadoc import proto_types


class PartnershipStatus(Enum):
Expand All @@ -26,10 +27,11 @@ class PartnershipStatus(Enum):

@dataclass(frozen=True)
class DiadocPartner(LegalEntity):
diadoc_id: str
is_active: bool
is_roaming: bool
diadoc_partnership_status: PartnershipStatus | None = None
diadoc_id: str = field(repr=False)
diadoc_box_id: str
is_active: bool = field(repr=False)
is_roaming: bool = field(repr=False)
diadoc_partnership_status: PartnershipStatus | None = field(default=None, repr=False)

@property
def in_partners(self):
Expand All @@ -39,27 +41,28 @@ def in_partners(self):
def invite_not_needed(self):
return self.diadoc_partnership_status in [PartnershipStatus.INVITE_WAS_SENT, PartnershipStatus.REJECTED]

@staticmethod
def from_organization(organization: DiadocOrganization, partnership_status: PartnershipStatus | None = None) -> "DiadocPartner":
return DiadocPartner(
@classmethod
def from_organization(cls, organization: proto_types.Organization, partnership_status: PartnershipStatus | None = None) -> Self:
return cls(
name=organization["ShortName"],
inn=organization["Inn"],
kpp=organization["Kpp"] or None,
diadoc_id=organization["OrgId"],
diadoc_box_id=organization["Boxes"][0]["BoxIdGuid"],
is_active=organization["IsActive"],
is_roaming=organization["IsRoaming"],
diadoc_partnership_status=partnership_status,
)

@classmethod
def from_organization_list(cls, organizations: list[DiadocOrganization]) -> list["DiadocPartner"]:
def from_organization_list(cls, organizations: list[proto_types.Organization]) -> list[Self]:
return [cls.from_organization(organization) for organization in organizations]

@classmethod
def from_counteragent(cls, counteragent: DiadocCounteragent) -> "DiadocPartner":
def from_counteragent(cls, counteragent: proto_types.Counteragent) -> Self:
partnership_status = COUNTERAGENT_PARTNERSHIP_STATUS_MAP[counteragent["CurrentStatus"]]
return cls.from_organization(counteragent["Organization"], partnership_status=partnership_status)

@classmethod
def from_counteragent_list(cls, counteragents: list[DiadocCounteragent]) -> list["DiadocPartner"]:
def from_counteragent_list(cls, counteragents: list[proto_types.Counteragent]) -> list[Self]:
return [cls.from_counteragent(counteragent) for counteragent in counteragents]
40 changes: 40 additions & 0 deletions src/diadoc/proto_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from typing import Literal, TypedDict


class Box(TypedDict):
"""https://developer.kontur.ru/docs/diadoc-api/proto/Box.html"""

BoxId: str # exists in response, but not used — according to docs BoxIdGuid should be used as boxId param
Title: str
BoxIdGuid: str


class Organization(TypedDict):
"""https://developer.kontur.ru/docs/diadoc-api/proto/Organization.html"""

ShortName: str
Inn: str
Kpp: str
OrgId: str
Boxes: list[Box] # boxes contains one and only one box
IsActive: bool
IsRoaming: bool


CounteragentStatus = Literal[
"UnknownCounteragentStatus",
"IsMyCounteragent",
"InvitesMe",
"IsInvitedByMe",
"RejectsMe",
"IsRejectedByMe",
"NotInCounteragentList",
]


class Counteragent(TypedDict):
"""https://developer.kontur.ru/docs/diadoc-api/proto/Counteragent.html"""

IndexKey: str
Organization: Organization
CurrentStatus: CounteragentStatus
1 change: 1 addition & 0 deletions src/diadoc/tests/client/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def my_organization():
inn="771245768212",
kpp=None,
diadoc_id="75fcac12-ec63-4cdd-9076-87a8a2e6e8ba",
diadoc_box_id="1ecf0eca-bfe3-4153-96a3-4ee0e72b69ed",
is_active=True,
is_roaming=False,
)
54 changes: 43 additions & 11 deletions src/diadoc/tests/client/tests_acquire_counteragent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,65 @@
import pytest
import re

from diadoc.client import DiadocClient
from diadoc.models import DiadocPartner


@pytest.fixture
def partner():
return DiadocPartner(
name="Кракозябра LTD",
inn="770099123001",
kpp=None,
diadoc_id="47bc8517-391d-43b4-9818-ab05d0cdb547",
diadoc_box_id="2c9d3a6d-d8b2-46fc-8bce-af99fc68c57",
is_active=True,
is_roaming=True,
)


@pytest.fixture
def mock_diadoc_response(httpx_mock):
return httpx_mock.add_response(
method="POST",
url=re.compile("https://diadoc-api.kontur.ru/V2/AcquireCounteragent.*"),
url=re.compile("https://diadoc-api.kontur.ru/V3/AcquireCounteragent.*"),
json={
"TaskId": "5ef95ef4-8ab9-4c1c-b333-6f78c6a43fd2",
},
)


@pytest.fixture
def mock_sentry_capture(mocker):
return mocker.patch("diadoc.client.capture_exception")
def mock_sentry_capture_message(mocker):
return mocker.patch("sentry_sdk.capture_message")


@pytest.fixture
def mock_sentry_capture_exc(mocker):
return mocker.patch("sentry_sdk.capture_exception")


@pytest.fixture
def acquire_counteragent(client, my_organization):
def acquire_counteragent(client: DiadocClient, my_organization, partner):
return partial(
client.acquire_counteragent,
my_diadoc_id=my_organization.diadoc_id,
diadoc_id="2c9d3a6d-d8b2-46fc-8bce-af99fc68c575",
my_organization=my_organization,
to_acquire=partner,
)


def test_my_org_id_in_query_params(acquire_counteragent, httpx_mock, mock_diadoc_response):
def test_my_org_box_id_in_query_params(acquire_counteragent, httpx_mock, mock_diadoc_response):
acquire_counteragent()

request_query = httpx_mock.get_request().url.query
assert b"myOrgId=75fcac12-ec63-4cdd-9076-87a8a2e6e8ba" in request_query
assert b"myBoxId=1ecf0eca-bfe3-4153-96a3-4ee0e72b69ed" in request_query


def test_payload_in_request_correct(acquire_counteragent, httpx_mock, mock_diadoc_response):
acquire_counteragent(message="Hello everyone!")

request_content = httpx_mock.get_request().content
assert request_content == b'{"OrgId":"2c9d3a6d-d8b2-46fc-8bce-af99fc68c575","MessageToCounteragent":"Hello everyone!"}'
assert request_content == b'{"BoxId":"2c9d3a6d-d8b2-46fc-8bce-af99fc68c57","MessageToCounteragent":"Hello everyone!"}'


@pytest.mark.parametrize(
Expand All @@ -54,10 +75,21 @@ def test_do_not_include_message_in_payload_if_empty(acquire_counteragent, httpx_
assert b"MessageToCounteragent" not in request_content


def test_do_not_raise_on_errors_and_capture_exception_to_sentry(acquire_counteragent, httpx_mock, mock_sentry_capture):
def test_do_not_raise_and_send_sentry_info_message_on_409_errors(acquire_counteragent, httpx_mock, mock_sentry_capture_message, mock_sentry_capture_exc):
httpx_mock.add_response(status_code=409, text="Setting is required for document exchange. Apply for roaming on www.diadoc.ru/roaming")

with does_not_raise():
acquire_counteragent()

mock_sentry_capture.assert_called_once()
mock_sentry_capture_message.assert_called_once()
mock_sentry_capture_exc.assert_not_called()


def test_do_not_raise_and_capture_sentry_exc_on_http_errors(acquire_counteragent, httpx_mock, mock_sentry_capture_message, mock_sentry_capture_exc):
httpx_mock.add_response(status_code=504, text="Just diadoc timeout message")

with does_not_raise():
acquire_counteragent()

mock_sentry_capture_exc.assert_called_once()
mock_sentry_capture_message.assert_not_called()
10 changes: 6 additions & 4 deletions src/diadoc/tests/client/tests_get_counteragents.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ def empty_page():
def mock_response(httpx_mock):
return partial(
httpx_mock.add_response,
url=re.compile("https://diadoc-api.kontur.ru/V2/GetCounteragents.*"),
url=re.compile("https://diadoc-api.kontur.ru/V3/GetCounteragents.*"),
)


@pytest.fixture
def get_counteragents(client, my_organization):
return partial(client.get_counteragents, my_diadoc_id=my_organization.diadoc_id)
return partial(client.get_counteragents, my_organization=my_organization)


def test_get_counteragents_return_diadoc_partners(get_counteragents, mock_response, page_one, empty_page):
Expand All @@ -46,6 +46,7 @@ def test_get_counteragents_return_diadoc_partners(get_counteragents, mock_respon
inn="123456789012",
kpp=None,
diadoc_id="229c4201-3680-4317-8a19-68d4748c0cd5",
diadoc_box_id="26af83e5-fabf-4805-95ed-7ef0fc16a4b8",
diadoc_partnership_status=PartnershipStatus.INVITE_SHOULD_BE_SEND,
is_active=True,
is_roaming=False,
Expand All @@ -55,6 +56,7 @@ def test_get_counteragents_return_diadoc_partners(get_counteragents, mock_respon
inn="7744001499",
kpp="997950001",
diadoc_id="aaddf4f0-6c13-4ddb-baf3-ad2265995365",
diadoc_box_id="8561678b-256a-4d51-a96a-bf255220867b",
diadoc_partnership_status=PartnershipStatus.ESTABLISHED,
is_active=True,
is_roaming=True,
Expand Down Expand Up @@ -94,13 +96,13 @@ def test_partnership_status_sets_correctly(get_counteragents, mock_response, pag
assert diadoc_partner.diadoc_partnership_status == expected


def test_my_org_id_param_in_every_request(get_counteragents, mock_response, empty_page, httpx_mock):
def test_my_org_box_id_param_in_every_request(get_counteragents, mock_response, empty_page, httpx_mock):
mock_response(json=empty_page)

get_counteragents()

request_query = httpx_mock.get_request().url.query
assert b"myOrgId=75fcac12-ec63-4cdd-9076-87a8a2e6e8ba" in request_query
assert b"myBoxId=1ecf0eca-bfe3-4153-96a3-4ee0e72b69ed" in request_query


def test_to_get_next_page_set_after_index_key_param(get_counteragents, mock_response, page_two, empty_page, httpx_mock):
Expand Down
2 changes: 2 additions & 0 deletions src/diadoc/tests/client/tests_get_my_organizations.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def test_get_my_organizations_return_diadoc_partners(client, mock_diadoc_respons
inn="123456789012",
kpp=None,
diadoc_id="229c4201-3680-4317-8a19-68d4748c0cd5",
diadoc_box_id="26af83e5-fabf-4805-95ed-7ef0fc16a4b8",
is_active=True,
is_roaming=False,
),
Expand All @@ -37,6 +38,7 @@ def test_get_my_organizations_return_diadoc_partners(client, mock_diadoc_respons
inn="7744001499",
kpp="997950001",
diadoc_id="aaddf4f0-6c13-4ddb-baf3-ad2265995365",
diadoc_box_id="8561678b-256a-4d51-a96a-bf255220867b",
is_active=True,
is_roaming=True,
),
Expand Down
2 changes: 2 additions & 0 deletions src/diadoc/tests/client/tests_get_organizations_by_inn_kpp.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def test_get_inn_kpp_organizations_return_diadoc_partners(get_organizations, moc
inn="7710140679",
kpp="771301001",
diadoc_id="8e165910-7ee8-4dc8-9d96-af7df4e8e0cd",
diadoc_box_id="2700ef9e-0108-4619-8d3e-74463cedc6c2",
is_active=True,
is_roaming=True,
),
Expand All @@ -38,6 +39,7 @@ def test_get_inn_kpp_organizations_return_diadoc_partners(get_organizations, moc
inn="7710140679",
kpp="771301001",
diadoc_id="a3e04137-55f3-4324-87d2-35c437a2d15a",
diadoc_box_id="4f7ce8b1-5761-4cb9-86ef-87ac132e45cb",
is_active=True,
is_roaming=False,
),
Expand Down
Loading

0 comments on commit 82f99e0

Please sign in to comment.