From 5e2c904f53990ae664e165e9fa59735cee5ed338 Mon Sep 17 00:00:00 2001 From: Tiago-da-Silva23 Date: Tue, 21 Nov 2023 18:20:24 +0000 Subject: [PATCH 1/2] test: created test to validate duplicate exception, feat: created validation for duplicate transaction exception --- .../services/financial_processor_service.py | 2 +- apps/billing/services/processor_service.py | 2 +- apps/billing/services/transaction_service.py | 42 +- .../billing/tests/test_transaction_service.py | 512 +++++++++++------- 4 files changed, 345 insertions(+), 213 deletions(-) diff --git a/apps/billing/services/financial_processor_service.py b/apps/billing/services/financial_processor_service.py index 37f6c3b..fba5ac5 100644 --- a/apps/billing/services/financial_processor_service.py +++ b/apps/billing/services/financial_processor_service.py @@ -8,7 +8,7 @@ class TransactionProcessorInterface: Each new transcation processor needs to implements its logic by signing to this class. """ - def send_transaction_to_processor(self, transaction: Transaction) -> str: + def send_transaction_to_processor(self, transaction: Transaction) -> dict: raise Exception("This method needs to be implemented") diff --git a/apps/billing/services/processor_service.py b/apps/billing/services/processor_service.py index 0931182..8f78cb4 100644 --- a/apps/billing/services/processor_service.py +++ b/apps/billing/services/processor_service.py @@ -25,7 +25,7 @@ def __init__(self) -> None: self.__user_processor_auth = getattr(settings, "USER_PROCESSOR_AUTH") self.__user_processor_password = getattr(settings, "USER_PROCESSOR_PASSWORD") - def send_transaction_to_processor(self, transaction: Transaction) -> str: + def send_transaction_to_processor(self, transaction: Transaction) -> dict: """ This method sends the transaction informations to the `Sage X3` service. """ diff --git a/apps/billing/services/transaction_service.py b/apps/billing/services/transaction_service.py index 042a21c..7395f96 100644 --- a/apps/billing/services/transaction_service.py +++ b/apps/billing/services/transaction_service.py @@ -1,3 +1,5 @@ +import xmltodict + from apps.billing.models import Transaction from apps.billing.services.financial_processor_service import ProcessorInstantiator, TransactionProcessorInterface from apps.billing.services.processor_service import SageX3Processor @@ -12,11 +14,47 @@ class TransactionService: def __init__(self) -> None: self.__processor: TransactionProcessorInterface = ProcessorInstantiator(processor=SageX3Processor) - def run_transaction_steps(self, transaction: Transaction): + def __check_transaction_state(self, document_id): + pass + + def __send_transaction_to_processor(self, transaction: Transaction) -> str: + """ + This method receives a Transaction to send to the processor and deals with the request result. + + From the result is extracted the document id, which is provided to invocate the `__check_transaction_state` + method, that checks and updates the transaction status. + """ + try: + response_from_service = self.__processor.send_transaction_to_processor(transaction=transaction) + response_from_service = response_from_service["soapenv:Envelope"]["soapenv:Body"] + + if "multiRef" in list(dict(response_from_service).keys()): + message = "Nº Fatura NAU já registada no documento: " + if message in response_from_service["multiRef"]["message"]: + document_id = response_from_service["multiRef"]["message"].replace(message, "") + self.__check_transaction_state(document_id=document_id) + + return document_id - document_id = self.__processor.send_transaction_to_processor(transaction=transaction) + result = xmltodict.parse(response_from_service["wss:saveResponse"]["saveReturn"]["resultXml"]["#text"]) + document_id = "" + for r in result["RESULT"]["GRP"]: + for field in r["FLD"]: + if field["@NAME"] == "NUM": + document_id = field["#text"] + break + + if document_id: + break return document_id except Exception as e: raise e + + def run_steps_to_send_transaction(self, transaction: Transaction) -> None: + try: + document_id = self.__send_transaction_to_processor(transaction=transaction) + self.__check_transaction_state(document_id=document_id) + except Exception as e: + raise e diff --git a/apps/billing/tests/test_transaction_service.py b/apps/billing/tests/test_transaction_service.py index f02849f..aa10d08 100644 --- a/apps/billing/tests/test_transaction_service.py +++ b/apps/billing/tests/test_transaction_service.py @@ -1,3 +1,5 @@ +from enum import Enum +from random import randint from unittest import mock import xmltodict @@ -16,6 +18,22 @@ transaction_item_data = {} +class MockResponse(Response): + def __init__(self, data, status_code): + self.data = data + self.status_code = status_code + + @property + def content(self): + return str(self.data) + + +class ProcessorResponseType(Enum): + SUCCESS = 1 + DUPLICATE_ERROR = 2 + GENERAL_ERROR = 3 + + def insert_in_dict(item: dict): """ This method insets in the right dict the payload informations. @@ -39,21 +57,12 @@ def insert_in_dict(item: dict): transaction_item_data[item["@NAME"]] = item["#text"] -def processor_response(*args, **kwargs): +def processor_success_response(*args, **kwargs): """ This method is a mock, when the `requests.post` method is called from the test, the result of the request will be returned from this method. """ - class MockResponse(Response): - def __init__(self, data, status_code): - self.data = data - self.status_code = status_code - - @property - def content(self): - return str(self.data) - received_data = xmltodict.parse(kwargs["data"])["soapenv:Envelope"]["soapenv:Body"]["wss:save"]["objectXml"] received_data = xmltodict.parse(received_data["#text"]) @@ -74,6 +83,16 @@ def content(self): billing_data=billing_data, client_data=client_data, transaction_item_data=transaction_item_data, + response_type=ProcessorResponseType.SUCCESS, + ) + + return MockResponse(response_as_xml, 200) + + +def processor_duplicate_error_response(*args, **kwargs): + response_as_xml = generate_data_to_response( + transaction_item_data=transaction_item_data, + response_type=ProcessorResponseType.DUPLICATE_ERROR, ) return MockResponse(response_as_xml, 200) @@ -119,7 +138,7 @@ def test_processor_instance(self): self.assertEqual(type(processor), SageX3Processor) self.assertTrue(isinstance(processor, TransactionProcessorInterface)) - @mock.patch("requests.post", side_effect=processor_response) + @mock.patch("requests.post", side_effect=processor_success_response) def test_transaction_processor(self, mocked_post): """ This test is a call for the processor service, it will use the written service with a mocked response. @@ -137,6 +156,34 @@ def test_transaction_processor(self, mocked_post): self.assertTrue(response) self.assertEqual(type(response), dict) + @mock.patch("requests.post", side_effect=processor_success_response) + def test_transaction_service_success_response(self, mocked_post): + """ + This method ensures the success result from the processor. + + Calling the `self.transaction_service.run_steps_to_send_transaction`, it deals with the success result + and extracts the document id from response payload. + """ + + fake_url_processor = "http://fake-processor.com" + setattr(settings, "TRANSACTION_PROCESSOR_URL", fake_url_processor) + + self.transaction_service.run_steps_to_send_transaction(transaction=self.transaction) + + @mock.patch("requests.post", side_effect=processor_duplicate_error_response) + def test_transaction_service_duplicate_error_response(self, mocked_post): + """ + This method ensures the duplicate result from the processor. + + Calling the `self.transaction_service.run_steps_to_send_transaction`, it deals with the duplicate result + and extracts the document id from response payload that indicates the duplicate informaation. + """ + + fake_url_processor = "http://fake-processor.com" + setattr(settings, "TRANSACTION_PROCESSOR_URL", fake_url_processor) + + self.transaction_service.run_steps_to_send_transaction(transaction=self.transaction) + def tearDown(self) -> None: """ This method is called in the last moment of the `TestCase` class and sets the `TRANSACTION_PROCESSOR_URL` @@ -147,226 +194,273 @@ def tearDown(self) -> None: def generate_data_to_response( - nau_data: dict, - billing_data: dict, - client_data: dict, transaction_item_data: dict, + response_type: ProcessorResponseType, + nau_data: dict = None, + billing_data: dict = None, + client_data: dict = None, ): - address_items = "" - for address in client_data["YBPAADDLIG"]: - address_items = f"{address_items}{address}" - response_as_xml = f""" + if response_type not in ProcessorResponseType: + raise "Invalid response type" + + if response_type == ProcessorResponseType.SUCCESS: + address_items = "" + for address in client_data["YBPAADDLIG"]: + address_items = f"{address_items}{address}" + + return f""" + + + + + + + + + SED + + {nau_data['SIVTYP']} + + FRN-23/000{randint(10, 99)} + {nau_data['INVREF']} + {nau_data['INVDAT']} + {nau_data['BPCINV']} + Cliente NAU via soap-ui 2 + {nau_data['CUR']} + + + 1 + + + + 1 + + + 1 + + + + + + {billing_data['VACBPR']} + + {billing_data['PRITYP']} + + + 1 + + 1 + 1 + + + PTTRFPP + + + + + + 0 + + + 0 + 0 + + + + + + 0 + 0 + + 0 + 0 + 0 + + + + 0 + + + + + + + + PT + Portugal + {client_data['YPOSCOD']} + {client_data['YCTY']} + {client_data['YBPIEECNUM']} + {client_data['YILINKMAIL']} + {client_data['YPAM']} + + Cliente NAU via soap-ui 1 + Cliente NAU via soap-ui 2 + + + {address_items} + + + + 20231109103351 + WSNAU + + + + {transaction_item_data['ITMREF']} + Genérico NAU - Formação Cientifca e Tec. + {transaction_item_data['ITMDES1']} + + + + 0 + UN + {transaction_item_data['QTY']} + 1 + {transaction_item_data['STU']} + {transaction_item_data['GROPRI']} + {transaction_item_data['DISCRGVAL1']} + 0 + 0 + 0 + 0 + 0 + 100 + 0 + 81.3008 + {transaction_item_data['VACITM1']} + + + + + + + + + + + + + + + + + + + + + + + + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + 1626.02 + 1626.02 + PT003 + 23 + 373.98 + 0 + 0 + + + ]]> + 1 + + false + false + false + false + 5 + -1 + 3 + 14948 + 5480 + -1 + 0 + + -1 + false + false + + 5507 + + 0 + + + + + + """ + + if response_type == ProcessorResponseType.DUPLICATE_ERROR: + + return f""" - - - - - SED - - {nau_data['SIVTYP']} - - FRN-23/00051 - {nau_data['INVREF']} - {nau_data['INVDAT']} - {nau_data['BPCINV']} - Cliente NAU via soap-ui 2 - {nau_data['CUR']} - - - 1 - - - - 1 - - - 1 - - - - - - {billing_data['VACBPR']} - - {billing_data['PRITYP']} - - - 1 - - 1 - 1 - - - PTTRFPP - - - - - - 0 - - - 0 - 0 - - - - - - 0 - 0 - - 0 - 0 - 0 - - - - 0 - - - - - - - - PT - Portugal - {client_data['YPOSCOD']} - {client_data['YCTY']} - {client_data['YBPIEECNUM']} - {client_data['YILINKMAIL']} - {client_data['YPAM']} - - Cliente NAU via soap-ui 1 - Cliente NAU via soap-ui 2 - - - {address_items} - - - - 20231109103351 - WSNAU - - - - {transaction_item_data['ITMREF']} - Genérico NAU - Formação Cientifca e Tec. - {transaction_item_data['ITMDES1']} - - - - 0 - UN - {transaction_item_data['QTY']} - 1 - {transaction_item_data['STU']} - {transaction_item_data['GROPRI']} - {transaction_item_data['DISCRGVAL1']} - 0 - 0 - 0 - 0 - 0 - 100 - 0 - 81.3008 - {transaction_item_data['VACITM1']} - - - - - - - - - - - - - - - - - - - - - - - - 1 - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0 - - 1626.02 - 1626.02 - PT003 - 23 - 373.98 - 0 - 0 - - - ]]> - 1 + + + + + 0 false false - false + true false - 5 + 36 -1 - 3 - 14948 - 5480 + 2 + 11260 + 16770 -1 - 0 + 1 -1 false false - 5507 + 16866 0 + + 3 + Nº Fatura NAU já registada no documento: FRN-{randint(21, 23)}/000{randint(10, 64)} + - """ - - return response_as_xml + """ From 7e2c8d49d33bc8b333087299c87226489d225c18 Mon Sep 17 00:00:00 2001 From: Tiago-da-Silva23 Date: Tue, 21 Nov 2023 18:49:15 +0000 Subject: [PATCH 2/2] feat: added validation to duplicate transaction and returning its document id --- apps/billing/services/transaction_service.py | 4 +-- .../billing/tests/test_transaction_service.py | 36 ++++++++++++++----- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/apps/billing/services/transaction_service.py b/apps/billing/services/transaction_service.py index 7395f96..53ea28c 100644 --- a/apps/billing/services/transaction_service.py +++ b/apps/billing/services/transaction_service.py @@ -17,7 +17,7 @@ def __init__(self) -> None: def __check_transaction_state(self, document_id): pass - def __send_transaction_to_processor(self, transaction: Transaction) -> str: + def send_transaction_to_processor(self, transaction: Transaction) -> str: """ This method receives a Transaction to send to the processor and deals with the request result. @@ -54,7 +54,7 @@ def __send_transaction_to_processor(self, transaction: Transaction) -> str: def run_steps_to_send_transaction(self, transaction: Transaction) -> None: try: - document_id = self.__send_transaction_to_processor(transaction=transaction) + document_id = self.send_transaction_to_processor(transaction=transaction) self.__check_transaction_state(document_id=document_id) except Exception as e: raise e diff --git a/apps/billing/tests/test_transaction_service.py b/apps/billing/tests/test_transaction_service.py index aa10d08..d8f72cc 100644 --- a/apps/billing/tests/test_transaction_service.py +++ b/apps/billing/tests/test_transaction_service.py @@ -157,26 +157,46 @@ def test_transaction_processor(self, mocked_post): self.assertEqual(type(response), dict) @mock.patch("requests.post", side_effect=processor_success_response) - def test_transaction_service_success_response(self, mocked_post): + def test_transaction_to_processor_success(self, mocked_post): """ - This method ensures the success result from the processor. + This test ensures the success result from the processor. - Calling the `self.transaction_service.run_steps_to_send_transaction`, it deals with the success result + Calling the `self.transaction_service.send_transaction_to_processor`, it deals with the success result and extracts the document id from response payload. """ fake_url_processor = "http://fake-processor.com" setattr(settings, "TRANSACTION_PROCESSOR_URL", fake_url_processor) - self.transaction_service.run_steps_to_send_transaction(transaction=self.transaction) + document_id = self.transaction_service.send_transaction_to_processor(transaction=self.transaction) + + self.assertTrue(isinstance(document_id, str)) + self.assertNotEqual(document_id, "") + self.assertTrue(document_id.startswith("FRN-")) @mock.patch("requests.post", side_effect=processor_duplicate_error_response) - def test_transaction_service_duplicate_error_response(self, mocked_post): + def test_transaction_to_processor_duplicate_error(self, mocked_post): + """ + This test ensures the duplicate result from the processor. + + Calling the `self.transaction_service.send_transaction_to_processor`, it deals with the duplicate result + and extracts the document id from response payload that indicates the duplicate information. """ - This method ensures the duplicate result from the processor. - Calling the `self.transaction_service.run_steps_to_send_transaction`, it deals with the duplicate result - and extracts the document id from response payload that indicates the duplicate informaation. + fake_url_processor = "http://fake-processor.com" + setattr(settings, "TRANSACTION_PROCESSOR_URL", fake_url_processor) + + document_id = self.transaction_service.send_transaction_to_processor(transaction=self.transaction) + + self.assertTrue(isinstance(document_id, str)) + self.assertNotEqual(document_id, "") + self.assertTrue(document_id.startswith("FRN-")) + + @mock.patch("requests.post", side_effect=processor_success_response) + def test_run_steps_to_send_transaction(self, mocked_post): + """ + This test ensures the success triggering the method that runs each step to send a + transaction to processor. """ fake_url_processor = "http://fake-processor.com"