From d0dd15453e192755e9d858b179da92aa71686e71 Mon Sep 17 00:00:00 2001 From: Andrey Oleynik Date: Tue, 4 Jun 2024 08:34:57 +0300 Subject: [PATCH] change zvonok call verification (#4393) # Change zvonok call verification After May 27, the Zvonok service will block number verification that was not set up as part of the phone number verification campaign. This PR modifies the number verification process. ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes. Co-authored-by: Innokentii Konstantinov --- docs/sources/set-up/open-source/index.md | 3 +- engine/apps/base/models/live_setting.py | 4 +- engine/apps/zvonok/phone_provider.py | 40 +++++++-------- .../apps/zvonok/tests/test_zvonok_provider.py | 50 ++++++++----------- engine/settings/base.py | 2 +- 5 files changed, 42 insertions(+), 57 deletions(-) diff --git a/docs/sources/set-up/open-source/index.md b/docs/sources/set-up/open-source/index.md index b925c7b3..734ec8fc 100644 --- a/docs/sources/set-up/open-source/index.md +++ b/docs/sources/set-up/open-source/index.md @@ -234,8 +234,7 @@ Zvonok.com, complete the following steps: to the variable `ZVONOK_AUDIO_ID` (optional step). 6. To make a call with a specific voice, you can set the `ZVONOK_SPEAKER_ID`. By default, the ID used is `Salli` (optional step). -7. To change the voice message for phone verification, you can set the variable `ZVONOK_VERIFICATION_TEMPLATE` - with the following format (optional step): `Your verification code is $verification_code, have a nice day.`. +7. Create phone number verification campaign with type `tellcode` and assign its ID value to `ZVONOK_VERIFICATION_CAMPAIGN_ID`. 8. To process the call status, it is required to add a postback with the GET/POST method on the side of the zvonok.com service with the following format (optional step): `${ONCALL_BASE_URL}/zvonok/call_status_events?campaign_id={ct_campaign_id}&call_id={ct_call_id}&status={ct_status}&user_choice={ct_user_choice}` diff --git a/engine/apps/base/models/live_setting.py b/engine/apps/base/models/live_setting.py index f6e8b7e5..7c9d39b0 100644 --- a/engine/apps/base/models/live_setting.py +++ b/engine/apps/base/models/live_setting.py @@ -71,7 +71,7 @@ class LiveSetting(models.Model): "ZVONOK_POSTBACK_STATUS", "ZVONOK_POSTBACK_USER_CHOICE", "ZVONOK_POSTBACK_USER_CHOICE_ACK", - "ZVONOK_VERIFICATION_TEMPLATE", + "ZVONOK_VERIFICATION_CAMPAIGN_ID", ) DESCRIPTIONS = { @@ -170,7 +170,7 @@ class LiveSetting(models.Model): "ZVONOK_POSTBACK_STATUS": "'Postback' status (ct_status) query parameter name to validate a postback request.", "ZVONOK_POSTBACK_USER_CHOICE": "'Postback' user choice (ct_user_choice) query parameter name (optional).", "ZVONOK_POSTBACK_USER_CHOICE_ACK": "'Postback' user choice (ct_user_choice) query parameter value for acknowledge alert group (optional).", - "ZVONOK_VERIFICATION_TEMPLATE": "The message template used for phone number verification (optional).", + "ZVONOK_VERIFICATION_CAMPAIGN_ID": "The phone number verification campaign ID. You can get it after verification campaign creation.", } SECRET_SETTING_NAMES = ( diff --git a/engine/apps/zvonok/phone_provider.py b/engine/apps/zvonok/phone_provider.py index e19e771f..4af75b6b 100644 --- a/engine/apps/zvonok/phone_provider.py +++ b/engine/apps/zvonok/phone_provider.py @@ -1,6 +1,5 @@ import logging from random import randint -from string import Template from typing import Optional import requests @@ -12,6 +11,7 @@ from apps.phone_notifications.phone_provider import PhoneProvider, ProviderFlags from apps.zvonok.models.phone_call import ZvonokCallStatuses, ZvonokPhoneCall ZVONOK_CALL_URL = "https://zvonok.com/manager/cabapi_external/api/v1/phones/call/" +ZVONOK_VERIFICATION_CALL_URL = "https://zvonok.com/manager/cabapi_external/api/v1/phones/tellcode/" logger = logging.getLogger(__name__) @@ -96,6 +96,15 @@ class ZvonokPhoneProvider(PhoneProvider): return requests.post(ZVONOK_CALL_URL, params=params) + def _verification_call_create(self, number: str, code: int): + params = { + "public_key": live_settings.ZVONOK_API_KEY, + "campaign_id": live_settings.ZVONOK_VERIFICATION_CAMPAIGN_ID, + "phone": number, + "pincode": code, + } + return requests.post(ZVONOK_VERIFICATION_CALL_URL, params=params) + def _get_graceful_msg(self, body, number): if body: status = body.get("status") @@ -105,34 +114,19 @@ class ZvonokPhoneProvider(PhoneProvider): return f"Failed make call to {number}" def make_verification_call(self, number: str): + body = None code = self._generate_verification_code() cache.set(self._cache_key(number), code, timeout=10 * 60) - codewspaces = " ".join(code) - body = None - speaker = live_settings.ZVONOK_SPEAKER_ID - - if live_settings.ZVONOK_VERIFICATION_TEMPLATE: - message = Template(live_settings.ZVONOK_VERIFICATION_TEMPLATE).safe_substitute( - verification_code=codewspaces + if not live_settings.ZVONOK_VERIFICATION_CAMPAIGN_ID: + raise FailedToStartVerification( + graceful_msg="Failed make verification call, verification campaign id not set." ) - else: - message = f"Your verification code is {codewspaces}" + try: - response = self._call_create( - number, - message, - speaker, - ) - response.raise_for_status() + response = self._verification_call_create(number, code) body = response.json() - if not body: - logger.error("ZvonokPhoneProvider.make_verification_call: failed, empty body") - raise FailedToMakeCall(graceful_msg=f"Failed make verification call to {number}, empty body") - - call_id = body.get("call_id") - if not call_id: - raise FailedToStartVerification(graceful_msg=self._get_graceful_msg(body, number)) + response.raise_for_status() except requests.exceptions.HTTPError as http_err: logger.error(f"ZvonokPhoneProvider.make_verification_call: failed {http_err}") raise FailedToStartVerification(graceful_msg=self._get_graceful_msg(body, number)) diff --git a/engine/apps/zvonok/tests/test_zvonok_provider.py b/engine/apps/zvonok/tests/test_zvonok_provider.py index 04eab9da..5e25045f 100644 --- a/engine/apps/zvonok/tests/test_zvonok_provider.py +++ b/engine/apps/zvonok/tests/test_zvonok_provider.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock, patch import pytest from django.test import override_settings +from apps.phone_notifications.exceptions import FailedToStartVerification from apps.zvonok.phone_provider import ZvonokPhoneProvider @@ -12,46 +13,37 @@ def provider(): @pytest.mark.django_db -def test_make_verification_call_with_template_set(provider): - verification_code = "123456" +def test_make_verification_call(provider): + verification_code = "123456789" number = "1234567890" - speaker_id = "Salli" - template_value = 'Your code is $verification_code' - excepted_message = 'Your code is 1 2 3 4 5 6' - - with override_settings(ZVONOK_VERIFICATION_TEMPLATE=template_value, ZVONOK_SPEAKER_ID=speaker_id): + campaign_id = "123456" + with override_settings(ZVONOK_VERIFICATION_CAMPAIGN_ID=campaign_id): with patch("django.core.cache.cache.set"): - provider._call_create = MagicMock(return_value=MagicMock(json=lambda: {"call_id": "12345"})) + provider._verification_call_create = MagicMock(return_value=MagicMock(json=lambda: {"status": "ok"})) provider._generate_verification_code = MagicMock(return_value=verification_code) provider.make_verification_call(number) - provider._call_create.assert_called_once_with(number, excepted_message, speaker_id) + provider._verification_call_create.assert_called_once_with(number, verification_code) @pytest.mark.django_db -def test_make_verification_call_with_invalid_template_set(provider): - verification_code = "123456" +def test_make_verification_call_without_campaign_id(provider): number = "1234567890" - speaker_id = "Salli" - template_value = "Your code is" - excepted_message = "Your code is" - - with override_settings(ZVONOK_VERIFICATION_TEMPLATE=template_value, ZVONOK_SPEAKER_ID=speaker_id): - with patch("django.core.cache.cache.set"): - provider._call_create = MagicMock(return_value=MagicMock(json=lambda: {"call_id": "12345"})) - provider._generate_verification_code = MagicMock(return_value=verification_code) + with patch("django.core.cache.cache.set"): + with pytest.raises(FailedToStartVerification): provider.make_verification_call(number) - provider._call_create.assert_called_once_with(number, excepted_message, speaker_id) @pytest.mark.django_db -def test_make_verification_call_without_template_set(provider): - verification_code = "123456" +def test_make_verification_call_with_error(provider): number = "1234567890" - speaker_id = "Salli" - excepted_message = "Your verification code is 1 2 3 4 5 6" - with override_settings(ZVONOK_SPEAKER_ID=speaker_id): + campaign_id = "123456" + + with override_settings(ZVONOK_VERIFICATION_CAMPAIGN_ID=campaign_id): with patch("django.core.cache.cache.set"): - provider._call_create = MagicMock(return_value=MagicMock(json=lambda: {"call_id": "12345"})) - provider._generate_verification_code = MagicMock(return_value=verification_code) - provider.make_verification_call(number) - provider._call_create.assert_called_once_with(number, excepted_message, speaker_id) + with pytest.raises(FailedToStartVerification): + provider._verification_call_create = MagicMock( + return_value=MagicMock( + json={"status": "error", "data": "Form isn't valid: * campaign_id\n * Invalid campaign type"} + ) + ) + provider.make_verification_call(number) diff --git a/engine/settings/base.py b/engine/settings/base.py index 48d3fbdd..c528ba3c 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -912,7 +912,7 @@ ZVONOK_POSTBACK_CAMPAIGN_ID = os.getenv("ZVONOK_POSTBACK_CAMPAIGN_ID", "campaign ZVONOK_POSTBACK_STATUS = os.getenv("ZVONOK_POSTBACK_STATUS", "status") ZVONOK_POSTBACK_USER_CHOICE = os.getenv("ZVONOK_POSTBACK_USER_CHOICE", None) ZVONOK_POSTBACK_USER_CHOICE_ACK = os.getenv("ZVONOK_POSTBACK_USER_CHOICE_ACK", None) -ZVONOK_VERIFICATION_TEMPLATE = os.getenv("ZVONOK_VERIFICATION_TEMPLATE", None) +ZVONOK_VERIFICATION_CAMPAIGN_ID = os.getenv("ZVONOK_VERIFICATION_CAMPAIGN_ID", None) DETACHED_INTEGRATIONS_SERVER = getenv_boolean("DETACHED_INTEGRATIONS_SERVER", default=False)