diff --git a/core/basic_models/actions/smartpay.py b/core/basic_models/actions/smartpay.py new file mode 100644 index 00000000..cbe383b1 --- /dev/null +++ b/core/basic_models/actions/smartpay.py @@ -0,0 +1,327 @@ +from typing import Optional, Dict, Union, List + +from core.basic_models.actions.command import Command +from core.model.base_user import BaseUser +from core.text_preprocessing.base import BaseTextPreprocessingResult +from smart_kit.action.http import HTTPRequestAction + + +class SmartPayAction(HTTPRequestAction): + url = None + method = None + store = None + + def __init__(self, items, id=None): + request_params = items.get("request_params") + smartpay_params = items.get("smartpay_params") + + items = { + "params": {"method": self.method, "url": self.url}, + "store": self.store, + "behavior": items["behavior"] + } + if request_params: + items["params"]["params"] = request_params + if smartpay_params: + items["params"]["json"] = smartpay_params + + super().__init__(items, id) + + def run(self, user: BaseUser, text_preprocessing_result: BaseTextPreprocessingResult, + params: Optional[Dict[str, Union[str, float, int]]] = None) -> Optional[List[Command]]: + self.http_action.method_params["url"] = \ + user.settings["template_settings"]["smartpay_url"] + self.http_action.method_params["url"] + return super().run(user, text_preprocessing_result, params) + + +class SmartPayCreateAction(SmartPayAction): + + """ + Example:: + { + "type": "smartpay_create", + "behavior": "some_behavior", // обязательный параметр + "smartpay_params": { // обязательноый параметр + "ptype": 1, + "invoice": { + "purchaser": { + "email": "test@test.ru", + "phone": "71111111111", + "contact": "phone" + }, + "delivery_info": { + "address": { + "country": "RU", + "city": "Москва", + "address": "Кутузовский проспект, 32" + }, + "delivery_type": "courier", + "description": "Спросить '\''Где тут Сбердевайсы?'\''" + }, + "invoice_params": [ + { + "key": "packageName", + "value": "SbERdeVICEs" + }, + { + "key": "packageKey", + "value": "ru.sberdevices.test" + } + ], + "order": { + "order_id": "a952b7ee-c928-4586-bd05-d932c21f749", + "order_number": "1952", + "order_date": "2021-04-07T08:24:37.729Z", + "service_id": "13", + "amount": 79900, + "currency": "RUB", + "purpose": "Покупка продуктов", + "description": "Заказ № 22-1952. Покупка продуктов", + "language": "ru-RU", + "tax_system": 0, + "order_bundle": [ + { + "position_id": 1, + "name": "Транзистор", + "item_params": [ + { + "key": "_itemAttributes_supplier_info.name", + "value": "ООО Ромашка" + }, + { + "key": "_itemAttributes_supplier_info.inn", + "value": "5009053292" + }, + { + "key": "_itemAttributes_nomenclature", + "value": "Å\u001e13622200005881" + }, + { + "key": "_itemAttributes_agent_info.type", + "value": "7" + } + + ], + "quantity": { + "value": 1, + "measure": "шт." + }, + "item_amount": 79801, + "currency": "RUB", + "item_code": "21ff407a-fa98-11e8-80c5-0cc47a817926", + "item_price": 79801, + "discount_type": "amount", + "discount_value": 17687, + "tax_type": 6 + }, + { + "position_id": 2, + "name": "Тиристор", + "item_params": [ + { + "key": "code", + "value": "123значение321" + }, + { + "key": "pack", + "value": "100" + }, + { + "key": "_itemAttributes_supplier_info.name", + "value": "ООО Платежи" + }, + { + "key": "_itemAttributes_supplier_info.inn", + "value": "7730253720" + } + ], + "quantity": { + "value": 1, + "measure": "шт." + }, + "item_amount": 99, + "currency": "RUB", + "item_code": "21ff407c-fa98-11e8-80c5-0cc47a817926", + "item_price": 99, + "discount_type": "amount", + "discount_value": 21, + "tax_type": 6, + } + ] + } + } + } + } + """ + + def __init__(self, items, id=None): + self.url = "/invoices" + self.method = self.POST + self.store = "smartpay_create_answer" + super().__init__(items, id) + + +class SmartPayPerformAction(SmartPayAction): + + """ + Example:: + { + "type": "smartpay_perform", + "behavior": "some_behavior", // обязательный параметр + "invoice_id": "0", // обязательный параметр + "smartpay_params": { // обязательынй параметр + "user_id": { + "partner_client_id": "2223" + }, + "operations": [ + { + "operation": "payment", + "code": "invoice", + "value": "167900" + } + ], + "device_info": { + "device_platform_type": "iOS", + "device_platform_version": "13.6.1", + "device_model": "iPhone 7", + "device_manufacturer": "Apple", + "device_id": "83c3f257-46d8-41fe-951b-f79d04e288c2", + "surface": "SBOL", + "surface_version": "11.5.0" + }, + "return_url": "https://ok", + "fail_url": "https://fail" + } + } + """ + + def __init__(self, items, id=None): + self.url = f"/invoices/{items['invoice_id']}" + self.method = self.POST + self.store = "smartpay_perform_answer" + super().__init__(items, id) + + +class SmartPayGetStatusAction(SmartPayAction): + + """ + Example:: + { + "type": "smartpay_get_status", + "behavior": "some_behavior", // обязательный параметр + "invoice_id": "0", // обязательный параметр + "inv_status": "executed", // опциональный параметр + "wait": 50 // опциональный параметр + } + """ + + def __init__(self, items, id=None): + items["request_params"] = {} + inv_status = items.get("inv_status") + wait = items.get("wait") + if inv_status: + del items["inv_status"] + items["request_params"]["inv_status"] = inv_status + if wait: + del items["wait"] + items["request_params"]["wait"] = wait + + self.url = f"/invoices/{items['invoice_id']}" + self.method = self.GET + self.store = "smartpay_get_status_answer" + super().__init__(items, id) + + +class SmartPayConfirmAction(SmartPayAction): + + """ + Example:: + { + "type": "smartpay_confirm", + "invoice_id": "0", // обязательный параметр + "smartpay_params": { // опциональный параметр, задаётся для неполной суммы (см. доку SmartPay API) + "invoice": { + "order": { + "amount": 79801, + "currency": "RUB", + "order_bundle": [ + { + "position_id": 1, + "name": "Транзистор", + "quantity": { + "value": 1, + "measure": "шт." + }, + "item_amount": 79801, + "currency": "RUB", + "item_code": "21ff407a-fa98-11e8-80c5-0cc47a817926", + "item_price": 79801 + } + ] + } + } + } + } + """ + + def __init__(self, items, id=None): + self.url = f"/invoices/{items['invoice_id']}" + self.method = self.PUT + self.store = "smartpay_confirm_answer" + super().__init__(items, id) + + +class SmartPayDeleteAction(SmartPayAction): + + """ + Example:: + { + "type": "smartpay_delete", + "behavior": "my_behavior", // обязательынй параметр + "invoice_id": "0" // обязательынй параметр + } + """ + + def __init__(self, items, id=None): + self.url = f"/invoices/{items['invoice_id']}" + self.method = self.DELETE + self.store = "smartpay_delete_answer" + super().__init__(items, id) + + +class SmartPayRefundAction(SmartPayAction): + + """ + Example:: + { + "type": "smartpay_refund", + "behavior": "some_behavior", // обязательный параметр + "invoice_id": "0", // обязательный параметр + "smartpay_params": { // опциональный параметр, задаётся при частичном возврате (см. доку SmartPay API) + "invoice": { + "order": { + "current_amount": 79900, + "refund_amount": 79801, + "currency": "RUB", + "order_bundle": [ + { + "position_id": 1, + "name": "Транзистор", + "quantity": { + "value": 1 + }, + "item_amount": 79801, + "item_code": "21ff407a-fa98-11e8-80c5-0cc47a817926" + } + ] + } + } + } + } + """ + + def __init__(self, items, id=None): + self.url = f"/invoices/{items['invoice_id']}" + self.method = self.PATCH + self.store = "smartpay_refund_answer" + super().__init__(items, id) diff --git a/smart_kit/action/base_http.py b/smart_kit/action/base_http.py index b2904309..5fcffdc0 100644 --- a/smart_kit/action/base_http.py +++ b/smart_kit/action/base_http.py @@ -64,8 +64,8 @@ def _make_response(self, request_parameters, user): except (requests.exceptions.ConnectionError, requests.exceptions.HTTPError): self.error = self.CONNECTION - def _get_requst_params(self, user: BaseUser, text_preprocessing_result: BaseTextPreprocessingResult, - params: Optional[Dict[str, Union[str, float, int]]] = None): + def _get_request_params(self, user: BaseUser, text_preprocessing_result: BaseTextPreprocessingResult, + params: Optional[Dict[str, Union[str, float, int]]] = None): collected = user.parametrizer.collect(text_preprocessing_result) params.update(collected) @@ -97,6 +97,6 @@ def _log_response(self, user, response, data): def run(self, user: BaseUser, text_preprocessing_result: BaseTextPreprocessingResult, params: Optional[Dict[str, Union[str, float, int]]] = None) -> Optional[List[Command]]: params = params or {} - request_parameters = self._get_requst_params(user, text_preprocessing_result, params) + request_parameters = self._get_request_params(user, text_preprocessing_result, params) self._log_request(user, request_parameters) return self._make_response(request_parameters, user) diff --git a/smart_kit/action/http.py b/smart_kit/action/http.py index 7437270c..0e3eb066 100644 --- a/smart_kit/action/http.py +++ b/smart_kit/action/http.py @@ -22,6 +22,11 @@ class HTTPRequestAction(Action): """ HTTP_ACTION = BaseHttpRequestAction + POST = "POST" + GET = "GET" + PUT = "PUT" + DELETE = "DELETE" + PATCH = "PATCH" def __init__(self, items, id=None): self.http_action = self.HTTP_ACTION(items["params"], id) diff --git a/smart_kit/resources/__init__.py b/smart_kit/resources/__init__.py index ba834ec8..0d050842 100644 --- a/smart_kit/resources/__init__.py +++ b/smart_kit/resources/__init__.py @@ -14,6 +14,8 @@ from core.basic_models.actions.external_actions import ExternalAction from core.basic_models.actions.external_actions import ExternalActions from core.basic_models.actions.push_action import PushAction, PUSH_NOTIFY +from core.basic_models.actions.smartpay import SmartPayCreateAction, SmartPayPerformAction, SmartPayGetStatusAction, \ + SmartPayConfirmAction, SmartPayDeleteAction, SmartPayRefundAction from core.basic_models.actions.string_actions import StringAction, AfinaAnswerAction, SDKAnswer, \ SDKAnswerToUser from core.basic_models.answer_items.answer_items import items_factory, SdkAnswerItem, answer_items, BubbleText, \ @@ -315,6 +317,12 @@ def init_actions(self): actions["push"] = PushAction actions["give_me_memory"] = GiveMeMemoryAction actions["remember_this"] = RememberThisAction + actions["smartpay_create"] = SmartPayCreateAction + actions["smartpay_perform"] = SmartPayPerformAction + actions["smartpay_get_status"] = SmartPayGetStatusAction + actions["smartpay_confirm"] = SmartPayConfirmAction + actions["smartpay_delete"] = SmartPayDeleteAction + actions["smartpay_refund"] = SmartPayRefundAction def init_requirements(self): requirements[None] = Requirement diff --git a/tests/core_tests/basic_scenario_models_test/action_test/test_smartpay.py b/tests/core_tests/basic_scenario_models_test/action_test/test_smartpay.py new file mode 100644 index 00000000..edfb6103 --- /dev/null +++ b/tests/core_tests/basic_scenario_models_test/action_test/test_smartpay.py @@ -0,0 +1,325 @@ +import unittest +from unittest.mock import patch + +from core.basic_models.actions.smartpay import SmartPayCreateAction, SmartPayPerformAction, SmartPayGetStatusAction, \ + SmartPayConfirmAction, SmartPayDeleteAction, SmartPayRefundAction +from smart_kit.utils.picklable_mock import PicklableMock +from tests.smart_kit_tests.action.test_base_http_action import BaseHttpRequestActionTest + + +class SmartPayActionTest(unittest.TestCase): + def setUp(self): + self.user = PicklableMock( + parametrizer=PicklableMock(collect=lambda *args, **kwargs: {}), + descriptions={ + "behaviors": { + "my_behavior": PicklableMock(timeout=PicklableMock(return_value=3)) + } + }, + settings={ + "template_settings": { + "smartpay_url": "0.0.0.0" + } + } + ) + + @patch('requests.request') + def test_create(self, request_mock: PicklableMock): + items = { + "behavior": "my_behavior", + "smartpay_params": { + "ptype": 1, + "invoice": { + "purchaser": { + "email": "test@test.ru", + "phone": "71111111111", + "contact": "phone" + }, + "delivery_info": { + "address": { + "country": "RU", + "city": "Москва", + "address": "Кутузовский проспект, 32" + }, + "delivery_type": "courier", + "description": "Спросить '\''Где тут Сбердевайсы?'\''" + }, + "invoice_params": [ + { + "key": "packageName", + "value": "SbERdeVICEs" + }, + { + "key": "packageKey", + "value": "ru.sberdevices.test" + } + ], + "order": { + "order_id": "a952b7ee-c928-4586-bd05-d932c21f749", + "order_number": "1952", + "order_date": "2021-04-07T08:24:37.729Z", + "service_id": "13", + "amount": 79900, + "currency": "RUB", + "purpose": "Покупка продуктов", + "description": "Заказ № 22-1952. Покупка продуктов", + "language": "ru-RU", + "tax_system": 0, + "order_bundle": [ + { + "position_id": 1, + "name": "Транзистор", + "item_params": [ + { + "key": "_itemAttributes_supplier_info.name", + "value": "ООО Ромашка" + }, + { + "key": "_itemAttributes_supplier_info.inn", + "value": "5009053292" + }, + { + "key": "_itemAttributes_nomenclature", + "value": "Å\u001e13622200005881" + }, + { + "key": "_itemAttributes_agent_info.type", + "value": "7" + } + + ], + "quantity": { + "value": 1, + "measure": "шт." + }, + "item_amount": 79801, + "currency": "RUB", + "item_code": "21ff407a-fa98-11e8-80c5-0cc47a817926", + "item_price": 79801, + "discount_type": "amount", + "discount_value": 17687, + "tax_type": 6 + }, + { + "position_id": 2, + "name": "Тиристор", + "item_params": [ + { + "key": "code", + "value": "123значение321" + }, + { + "key": "pack", + "value": "100" + }, + { + "key": "_itemAttributes_supplier_info.name", + "value": "ООО Платежи" + }, + { + "key": "_itemAttributes_supplier_info.inn", + "value": "7730253720" + } + ], + "quantity": { + "value": 1, + "measure": "шт." + }, + "item_amount": 99, + "currency": "RUB", + "item_code": "21ff407c-fa98-11e8-80c5-0cc47a817926", + "item_price": 99, + "discount_type": "amount", + "discount_value": 21, + "tax_type": 6, + } + ] + } + } + } + } + + BaseHttpRequestActionTest.set_request_mock_attribute(request_mock, return_value={'data': 'value'}) + action = SmartPayCreateAction(items) + action.run(self.user, None, {}) + request_mock.assert_called_with(url="0.0.0.0/invoices", method="POST", + timeout=3, json=items.get("smartpay_params")) + self.assertTrue(self.user.descriptions["behaviors"]["my_behavior"].success_action.run.called) + self.assertTrue(self.user.variables.set.called) + self.user.variables.set.assert_called_with("smartpay_create_answer", {'data': 'value'}) + + @patch('requests.request') + def test_perform(self, request_mock: PicklableMock): + items = { + "behavior": "my_behavior", + "invoice_id": "0", + "smartpay_params": { + "user_id": { + "partner_client_id": "2223" + }, + "operations": [ + { + "operation": "payment", + "code": "invoice", + "value": "167900" + } + ], + "device_info": { + "device_platform_type": "iOS", + "device_platform_version": "13.6.1", + "device_model": "iPhone 7", + "device_manufacturer": "Apple", + "device_id": "83c3f257-46d8-41fe-951b-f79d04e288c2", + "surface": "SBOL", + "surface_version": "11.5.0" + }, + "return_url": "https://ok", + "fail_url": "https://fail" + } + } + + BaseHttpRequestActionTest.set_request_mock_attribute(request_mock, return_value={'data': 'value'}) + action = SmartPayPerformAction(items) + action.run(self.user, None, {}) + request_mock.assert_called_with(url="0.0.0.0/invoices/0", method="POST", + timeout=3, json=items.get("smartpay_params")) + self.assertTrue(self.user.descriptions["behaviors"]["my_behavior"].success_action.run.called) + self.assertTrue(self.user.variables.set.called) + self.user.variables.set.assert_called_with("smartpay_perform_answer", {'data': 'value'}) + + @patch('requests.request') + def test_get_status(self, request_mock: PicklableMock): + items = { + "behavior": "my_behavior", + "invoice_id": "0", + "inv_status": "executed", + "wait": 50 + } + + BaseHttpRequestActionTest.set_request_mock_attribute(request_mock, return_value={'data': 'value'}) + action = SmartPayGetStatusAction(items) + action.run(self.user, None, {}) + request_mock.assert_called_with(url="0.0.0.0/invoices/0", method="GET", + timeout=3, params={"inv_status": "executed", "wait": 50}) + self.assertTrue(self.user.descriptions["behaviors"]["my_behavior"].success_action.run.called) + self.assertTrue(self.user.variables.set.called) + self.user.variables.set.assert_called_with("smartpay_get_status_answer", {'data': 'value'}) + + @patch('requests.request') + def test_partial_confirm(self, request_mock: PicklableMock): + items = { + "behavior": "my_behavior", + "invoice_id": "0", + "smartpay_params": { + "invoice": { + "order": { + "amount": 79801, + "currency": "RUB", + "order_bundle": [ + { + "position_id": 1, + "name": "Транзистор", + "quantity": { + "value": 1, + "measure": "шт." + }, + "item_amount": 79801, + "currency": "RUB", + "item_code": "21ff407a-fa98-11e8-80c5-0cc47a817926", + "item_price": 79801 + } + ] + } + } + } + } + + BaseHttpRequestActionTest.set_request_mock_attribute(request_mock, return_value={'data': 'value'}) + action = SmartPayConfirmAction(items) + action.run(self.user, None, {}) + request_mock.assert_called_with(url="0.0.0.0/invoices/0", method="PUT", + timeout=3, json=items["smartpay_params"]) + self.assertTrue(self.user.descriptions["behaviors"]["my_behavior"].success_action.run.called) + self.assertTrue(self.user.variables.set.called) + self.user.variables.set.assert_called_with("smartpay_confirm_answer", {'data': 'value'}) + + @patch('requests.request') + def test_full_confirm(self, request_mock: PicklableMock): + items = { + "behavior": "my_behavior", + "invoice_id": "0", + } + + BaseHttpRequestActionTest.set_request_mock_attribute(request_mock, return_value={'data': 'value'}) + action = SmartPayConfirmAction(items) + action.run(self.user, None, {}) + request_mock.assert_called_with(url="0.0.0.0/invoices/0", method="PUT", timeout=3) + self.assertTrue(self.user.descriptions["behaviors"]["my_behavior"].success_action.run.called) + self.assertTrue(self.user.variables.set.called) + self.user.variables.set.assert_called_with("smartpay_confirm_answer", {'data': 'value'}) + + @patch('requests.request') + def test_delete(self, request_mock: PicklableMock): + items = { + "behavior": "my_behavior", + "invoice_id": "0", + } + + BaseHttpRequestActionTest.set_request_mock_attribute(request_mock, return_value={'data': 'value'}) + action = SmartPayDeleteAction(items) + action.run(self.user, None, {}) + request_mock.assert_called_with(url="0.0.0.0/invoices/0", method="DELETE", timeout=3) + self.assertTrue(self.user.descriptions["behaviors"]["my_behavior"].success_action.run.called) + self.assertTrue(self.user.variables.set.called) + self.user.variables.set.assert_called_with("smartpay_delete_answer", {'data': 'value'}) + + @patch('requests.request') + def test_partial_refund(self, request_mock: PicklableMock): + items = { + "behavior": "my_behavior", + "invoice_id": "0", + "smartpay_params": { + "invoice": { + "order": { + "current_amount": 79900, + "refund_amount": 79801, + "currency": "RUB", + "order_bundle": [ + { + "position_id": 1, + "name": "Транзистор", + "quantity": { + "value": 1 + }, + "item_amount": 79801, + "item_code": "21ff407a-fa98-11e8-80c5-0cc47a817926" + } + ] + } + } + } + } + + BaseHttpRequestActionTest.set_request_mock_attribute(request_mock, return_value={'data': 'value'}) + action = SmartPayRefundAction(items) + action.run(self.user, None, {}) + request_mock.assert_called_with(url="0.0.0.0/invoices/0", method="PATCH", + timeout=3, json=items["smartpay_params"]) + self.assertTrue(self.user.descriptions["behaviors"]["my_behavior"].success_action.run.called) + self.assertTrue(self.user.variables.set.called) + self.user.variables.set.assert_called_with("smartpay_refund_answer", {'data': 'value'}) + + @patch('requests.request') + def test_full_refund(self, request_mock: PicklableMock): + items = { + "behavior": "my_behavior", + "invoice_id": "0", + } + + BaseHttpRequestActionTest.set_request_mock_attribute(request_mock, return_value={'data': 'value'}) + action = SmartPayRefundAction(items) + action.run(self.user, None, {}) + request_mock.assert_called_with(url="0.0.0.0/invoices/0", method="PATCH", timeout=3) + self.assertTrue(self.user.descriptions["behaviors"]["my_behavior"].success_action.run.called) + self.assertTrue(self.user.variables.set.called) + self.user.variables.set.assert_called_with("smartpay_refund_answer", {'data': 'value'}) diff --git a/tests/smart_kit_tests/action/test_base_http_action.py b/tests/smart_kit_tests/action/test_base_http_action.py index 581b0a70..cb6977a3 100644 --- a/tests/smart_kit_tests/action/test_base_http_action.py +++ b/tests/smart_kit_tests/action/test_base_http_action.py @@ -2,6 +2,7 @@ from unittest.mock import Mock, patch from smart_kit.action.base_http import BaseHttpRequestAction +from smart_kit.utils.picklable_mock import PicklableMock class BaseHttpRequestActionTest(unittest.TestCase): @@ -11,13 +12,13 @@ def setUp(self): @staticmethod def set_request_mock_attribute(request_mock, return_value=None): return_value = return_value or {} - request_mock.return_value = Mock( - __enter__=Mock(return_value=Mock( - json=Mock(return_value=return_value), + request_mock.return_value = PicklableMock( + __enter__=PicklableMock(return_value=PicklableMock( + json=PicklableMock(return_value=return_value), cookies={}, headers={}, ),), - __exit__=Mock() + __exit__=PicklableMock() ) @patch('requests.request')