From 82e7a9c7b59834000199ea8f0d8f0d1d9e8b2528 Mon Sep 17 00:00:00 2001 From: Tiago-Salles Date: Thu, 31 Oct 2024 17:35:37 +0000 Subject: [PATCH] refactor: retry sending transaction to transaction service layer - Removed the command functionality from the command layer - Added tests to the command logic --- .../commands/retry_sage_transactions.py | 25 +- apps/billing/services/transaction_service.py | 42 ++ .../test_command_retry_sage_transactions.py | 337 ++++++++++++-- .../billing/tests/test_transaction_service.py | 415 +++++++++++++++++- 4 files changed, 764 insertions(+), 55 deletions(-) diff --git a/apps/billing/management/commands/retry_sage_transactions.py b/apps/billing/management/commands/retry_sage_transactions.py index e2e3bc4..6b3d9ae 100644 --- a/apps/billing/management/commands/retry_sage_transactions.py +++ b/apps/billing/management/commands/retry_sage_transactions.py @@ -2,7 +2,6 @@ from django.core.management.base import BaseCommand -from apps.billing.models import SageX3TransactionInformation from apps.billing.services.transaction_service import TransactionService @@ -30,32 +29,12 @@ def add_arguments(self, parser): "--transaction_id", type=str, required=False, help="The transaction_id to retry to send to SageX3" ) - @staticmethod - def _sagex3_transaction_info_query(transaction_id): - if transaction_id: - return SageX3TransactionInformation.objects.filter(transaction__transaction_id=transaction_id) - else: - return SageX3TransactionInformation.objects.filter( - status__in=[SageX3TransactionInformation.FAILED, SageX3TransactionInformation.PENDING] - ) - def handle(self, *args, **kwargs) -> str | None: transaction_id = kwargs["transaction_id"] start = time.time() self.stdout.write("\nGetting failed transactions with Sage X3...\n") - sagex3_to_retry = self.__class__._sagex3_transaction_info_query(transaction_id) - total_count = sagex3_to_retry.count() - counters = {"success": 0, "failed": 0} - try: - for sagex3_failed_transaction in sagex3_to_retry: - if TransactionService(sagex3_failed_transaction.transaction).run_steps_to_send_transaction(): - counters["success"] += 1 - else: - counters["failed"] += 1 - except Exception as e: - self.stdout.write(f"Error while retrying: {e}") - counters["failed"] += 1 + counters = TransactionService.retry_sending_transactions(transaction_id=transaction_id) finish = time.time() - start - self.stdout.write(f"\n----- {total_count} Transactions were retried -----\n") + self.stdout.write(f"\n----- {counters['total_count']} Transactions were retried -----\n") self.stdout.write(f"\nSUCCESSFULL RETRIES: {counters['success']} FAILED RETRIES: {counters['failed']}\n") self.stdout.write(f"\nThe time to retry all transactions was {finish}\n") diff --git a/apps/billing/services/transaction_service.py b/apps/billing/services/transaction_service.py index 57cc67d..25246cd 100644 --- a/apps/billing/services/transaction_service.py +++ b/apps/billing/services/transaction_service.py @@ -130,3 +130,45 @@ def run_steps_to_send_transaction(self) -> bool: transaction=self.transaction, ) return False + + @staticmethod + def sagex3_transaction_info_query(transaction_id: str | None): + """ + This method returns the transactions that need to be retried and if + a `transaction_id` was provided, it will return only the corresponding transaction. + + This method must always return a list, regardless of its length. + """ + + if transaction_id: + result = SageX3TransactionInformation.objects.filter(transaction__transaction_id=transaction_id) + + return list(result) + + result = SageX3TransactionInformation.objects.filter( + status__in=[SageX3TransactionInformation.FAILED, SageX3TransactionInformation.PENDING] + ) + + return list(result) + + @staticmethod + def retry_sending_transactions(transaction_id: str | None): + """ + This method retries sending transactions and also is + possible to retry an specific one by providing the `transaction_id`. + """ + + sagex3_to_retry = TransactionService.sagex3_transaction_info_query(transaction_id) + counters = {"success": 0, "failed": 0, "total_count": len(sagex3_to_retry)} + + for sagex3_failed_transaction in sagex3_to_retry: + try: + if TransactionService(sagex3_failed_transaction.transaction).run_steps_to_send_transaction(): + counters["success"] += 1 + else: + counters["failed"] += 1 + except Exception as e: + print(f"Error while retrying: {e}") + counters["failed"] += 1 + + return counters diff --git a/apps/billing/tests/test_command_retry_sage_transactions.py b/apps/billing/tests/test_command_retry_sage_transactions.py index f276a34..0e9cbce 100644 --- a/apps/billing/tests/test_command_retry_sage_transactions.py +++ b/apps/billing/tests/test_command_retry_sage_transactions.py @@ -1,49 +1,324 @@ +from io import StringIO from unittest import mock from django.core.management import call_command -from django.test import TestCase +from django.test import TestCase, override_settings from apps.billing.factories import SageX3TransactionInformationFactory, TransactionFactory, TransactionItemFactory +from apps.billing.mocks import MockResponse from apps.billing.models import SageX3TransactionInformation +from apps.billing.tests.test_transaction_service import raise_timeout +from apps.billing.tests.test_utils import processor_duplicate_error_response, processor_success_response -def create_transaction(status): - transaction = TransactionFactory.build() - transaction.save() - item = TransactionItemFactory.build(transaction=transaction) - item.save() - sageX3TI = SageX3TransactionInformationFactory.build(transaction=transaction, status=status) - sageX3TI.save() - return transaction - - -@mock.patch("apps.billing.services.transaction_service.TransactionService", autospec=True) class CommandRetrySageTransactionsTestCase(TestCase): """ Test the `retry_sage_transactions` Django command. """ - def test_command_retry_sage_transactions(self, transaction_service_mock): + @override_settings(TRANSACTION_PROCESSOR_URL="http://fake-processor.com", DEFAULT_SERIES="AAA") + @mock.patch("requests.post", side_effect=processor_success_response) + def test_command_retry_sage_transactions_no_transactions(self, mocked_post): + """ + This test ensures the custom command functionality with no transactions to retry. + """ + + out = StringIO() + + call_command("retry_sage_transactions", stdout=out) + + message = "----- 0 Transactions were retried -----\n\nSUCCESSFULL RETRIES: 0 FAILED RETRIES: 0" + + self.assertTrue(message in out.getvalue()) + + @override_settings(TRANSACTION_PROCESSOR_URL="http://fake-processor.com", DEFAULT_SERIES="AAA") + @mock.patch("requests.post", side_effect=processor_success_response) + def test_command_retry_sage_transactions_not_found_transaction(self, mocked_post): + """ + This test ensures the custom command functionality with a not found `transaction_id`. + """ + + out = StringIO() + + call_command("retry_sage_transactions", "--transaction_id=WRONG_TRANSACTION_ID", stdout=out) + + message = "----- 0 Transactions were retried -----\n\nSUCCESSFULL RETRIES: 0 FAILED RETRIES: 0" + + self.assertTrue(message in out.getvalue()) + + @override_settings(TRANSACTION_PROCESSOR_URL="http://fake-processor.com", DEFAULT_SERIES="AAA") + @mock.patch("requests.post", side_effect=processor_success_response) + def test_command_retry_sage_transactions_success_PENDIND_status(self, mocked_post): + """ + This test ensures the custom command functionality for a transaction with `PENDING` status. + """ + + out = StringIO() + transaction = TransactionFactory.create() + TransactionItemFactory.create(transaction=transaction) + + SageX3TransactionInformationFactory.create( + transaction=transaction, status=SageX3TransactionInformation.PENDING + ) + + call_command("retry_sage_transactions", f"--transaction_id={transaction.transaction_id}", stdout=out) + + message = "----- 1 Transactions were retried -----\n\nSUCCESSFULL RETRIES: 1 FAILED RETRIES: 0" + + self.assertTrue(message in out.getvalue()) + + @override_settings(TRANSACTION_PROCESSOR_URL="http://fake-processor.com", DEFAULT_SERIES="AAA") + @mock.patch("requests.post", side_effect=processor_success_response) + def test_command_retry_sage_transactions_success_FAILED_status(self, mocked_post): + """ + This test ensures the custom command functionality for a transaction with `FAILED` status. + """ + + out = StringIO() + transaction = TransactionFactory.create() + TransactionItemFactory.create(transaction=transaction) + + SageX3TransactionInformationFactory.create(transaction=transaction, status=SageX3TransactionInformation.FAILED) + + call_command("retry_sage_transactions", f"--transaction_id={transaction.transaction_id}", stdout=out) + + message = "----- 1 Transactions were retried -----\n\nSUCCESSFULL RETRIES: 1 FAILED RETRIES: 0" + + self.assertTrue(message in out.getvalue()) + + @override_settings(TRANSACTION_PROCESSOR_URL="http://fake-processor.com", DEFAULT_SERIES="AAA") + @mock.patch("requests.post", side_effect=processor_success_response) + def test_command_retry_sage_transactions_success_without_transaction_id(self, mocked_post): + """ + This test ensures the custom command functionality without providing a `transaction_id`. + """ + + out = StringIO() + + for transaction in TransactionFactory.create_batch(10): + TransactionItemFactory.create(transaction=transaction) + SageX3TransactionInformationFactory.create( + transaction=transaction, status=SageX3TransactionInformation.FAILED + ) + + for transaction in TransactionFactory.create_batch(10): + TransactionItemFactory.create(transaction=transaction) + SageX3TransactionInformationFactory.create( + transaction=transaction, status=SageX3TransactionInformation.PENDING + ) + + call_command("retry_sage_transactions", stdout=out) + + message = "----- 20 Transactions were retried -----\n\nSUCCESSFULL RETRIES: 20 FAILED RETRIES: 0" + + self.assertTrue(message in out.getvalue()) + + @override_settings(TRANSACTION_PROCESSOR_URL="http://fake-processor.com", DEFAULT_SERIES="AAA") + @mock.patch("requests.post", return_value=MockResponse(status_code=500, data="Some not expected error")) + def test_command_retry_sage_transactions_error_PENDIND_status(self, mocked_post): + """ + This test ensures the custom command functionality receiving an internal server error + from `SageX3` retrying a transaction with `PENDING` status. + """ + + out = StringIO() + transaction = TransactionFactory.create() + TransactionItemFactory.create(transaction=transaction) + + SageX3TransactionInformationFactory.create( + transaction=transaction, status=SageX3TransactionInformation.PENDING + ) + + call_command("retry_sage_transactions", f"--transaction_id={transaction.transaction_id}", stdout=out) + + message = "----- 1 Transactions were retried -----\n\nSUCCESSFULL RETRIES: 0 FAILED RETRIES: 1" + + self.assertTrue(message in out.getvalue()) + + @override_settings(TRANSACTION_PROCESSOR_URL="http://fake-processor.com", DEFAULT_SERIES="AAA") + @mock.patch("requests.post", return_value=MockResponse(status_code=500, data="Some not expected error")) + def test_command_retry_sage_transactions_error_FAILED_status(self, mocked_post): + """ + This test ensures the custom command functionality receiving an internal server error + from `SageX3` retrying a transaction with `FAILED` status. + """ + + out = StringIO() + transaction = TransactionFactory.create() + TransactionItemFactory.create(transaction=transaction) + + SageX3TransactionInformationFactory.create(transaction=transaction, status=SageX3TransactionInformation.FAILED) + + call_command("retry_sage_transactions", f"--transaction_id={transaction.transaction_id}", stdout=out) + + message = "----- 1 Transactions were retried -----\n\nSUCCESSFULL RETRIES: 0 FAILED RETRIES: 1" + + self.assertTrue(message in out.getvalue()) + + @override_settings(TRANSACTION_PROCESSOR_URL="http://fake-processor.com", DEFAULT_SERIES="AAA") + @mock.patch("requests.post", return_value=MockResponse(status_code=500, data="Some not expected error")) + def test_command_retry_sage_transactions_error_without_transaction_id(self, mocked_post): + """ + This test ensures the custom command functionality receiving an internal server error + from `SageX3` without providing a `transaction_id`. + """ + + out = StringIO() + + for transaction in TransactionFactory.create_batch(10): + TransactionItemFactory.create(transaction=transaction) + SageX3TransactionInformationFactory.create( + transaction=transaction, status=SageX3TransactionInformation.FAILED + ) + + for transaction in TransactionFactory.create_batch(10): + TransactionItemFactory.create(transaction=transaction) + SageX3TransactionInformationFactory.create( + transaction=transaction, status=SageX3TransactionInformation.PENDING + ) + + call_command("retry_sage_transactions", stdout=out) + + message = "----- 20 Transactions were retried -----\n\nSUCCESSFULL RETRIES: 0 FAILED RETRIES: 20" + + self.assertTrue(message in out.getvalue()) + + @override_settings(TRANSACTION_PROCESSOR_URL="http://fake-processor.com") + @mock.patch("requests.post", side_effect=raise_timeout) + def test_command_retry_sage_transactions_timeout_error_PENDIND_status(self, mocked_post): + """ + This test ensures the custom command functionality receiving a timeout error + from `SageX3` retrying a transaction with `PENDING` status. + """ + + out = StringIO() + transaction = TransactionFactory.create() + TransactionItemFactory.create(transaction=transaction) + + SageX3TransactionInformationFactory.create( + transaction=transaction, status=SageX3TransactionInformation.PENDING + ) + + call_command("retry_sage_transactions", f"--transaction_id={transaction.transaction_id}", stdout=out) + + message = "----- 1 Transactions were retried -----\n\nSUCCESSFULL RETRIES: 0 FAILED RETRIES: 1" + + self.assertTrue(message in out.getvalue()) + + @override_settings(TRANSACTION_PROCESSOR_URL="http://fake-processor.com") + @mock.patch("requests.post", side_effect=raise_timeout) + def test_command_retry_sage_transactions_timeout_error_FAILED_status(self, mocked_post): + """ + This test ensures the custom command functionality receiving a timeout error + from `SageX3` retrying a transaction with `FAILED` status. + """ + + out = StringIO() + transaction = TransactionFactory.create() + TransactionItemFactory.create(transaction=transaction) + + SageX3TransactionInformationFactory.create(transaction=transaction, status=SageX3TransactionInformation.FAILED) + + call_command("retry_sage_transactions", f"--transaction_id={transaction.transaction_id}", stdout=out) + + message = "----- 1 Transactions were retried -----\n\nSUCCESSFULL RETRIES: 0 FAILED RETRIES: 1" + + self.assertTrue(message in out.getvalue()) + + @override_settings(TRANSACTION_PROCESSOR_URL="http://fake-processor.com") + @mock.patch("requests.post", side_effect=raise_timeout) + def test_command_retry_sage_transactions_timeout_error_without_transaction_id(self, mocked_post): """ - Test the command `retry_sage_transactions`. - Unfortunately all this calls needs to be on the same test. + This test ensures the custom command functionality receiving a timeout error + from `SageX3` without providing a `transaction_id`. """ - call_command("retry_sage_transactions") - transaction_service_mock.assert_not_called() - # test a pending transaction should be retried - t = create_transaction(SageX3TransactionInformation.PENDING) - for _ in range(5): - create_transaction(SageX3TransactionInformation.SUCCESS) - call_command("retry_sage_transactions") - transaction_service_mock.assert_called_once_with(t) + out = StringIO() + + for transaction in TransactionFactory.create_batch(10): + TransactionItemFactory.create(transaction=transaction) + SageX3TransactionInformationFactory.create( + transaction=transaction, status=SageX3TransactionInformation.FAILED + ) + + for transaction in TransactionFactory.create_batch(10): + TransactionItemFactory.create(transaction=transaction) + SageX3TransactionInformationFactory.create( + transaction=transaction, status=SageX3TransactionInformation.PENDING + ) + + call_command("retry_sage_transactions", stdout=out) + + message = "----- 20 Transactions were retried -----\n\nSUCCESSFULL RETRIES: 0 FAILED RETRIES: 20" + + self.assertTrue(message in out.getvalue()) + + @override_settings(TRANSACTION_PROCESSOR_URL="http://fake-processor.com", DEFAULT_SERIES="AAA") + @mock.patch("requests.post", side_effect=processor_duplicate_error_response) + def test_command_retry_sage_transactions_duplicate_error_PENDING_status(self, mocked_post): + """ + This test ensures the custom command functionality receiving a duplicate error response + for a transaction with `PENDING` status. + """ + + out = StringIO() + transaction = TransactionFactory.create() + TransactionItemFactory.create(transaction=transaction) + + SageX3TransactionInformationFactory.create( + transaction=transaction, status=SageX3TransactionInformation.PENDING + ) + + call_command("retry_sage_transactions", f"--transaction_id={transaction.transaction_id}", stdout=out) + + message = "----- 1 Transactions were retried -----\n\nSUCCESSFULL RETRIES: 1 FAILED RETRIES: 0" + + self.assertTrue(message in out.getvalue()) + + @override_settings(TRANSACTION_PROCESSOR_URL="http://fake-processor.com", DEFAULT_SERIES="AAA") + @mock.patch("requests.post", side_effect=processor_duplicate_error_response) + def test_command_retry_sage_transactions_duplicate_error_FAILED_status(self, mocked_post): + """ + This test ensures the custom command functionality receiving a duplicate error response + for a transaction with `FAILED` status. + """ + + out = StringIO() + transaction = TransactionFactory.create() + TransactionItemFactory.create(transaction=transaction) + + SageX3TransactionInformationFactory.create(transaction=transaction, status=SageX3TransactionInformation.FAILED) + + call_command("retry_sage_transactions", f"--transaction_id={transaction.transaction_id}", stdout=out) + + message = "----- 1 Transactions were retried -----\n\nSUCCESSFULL RETRIES: 1 FAILED RETRIES: 0" + + self.assertTrue(message in out.getvalue()) + + @override_settings(TRANSACTION_PROCESSOR_URL="http://fake-processor.com", DEFAULT_SERIES="AAA") + @mock.patch("requests.post", side_effect=processor_duplicate_error_response) + def test_command_retry_sage_transactions_duplicate_error(self, mocked_post): + """ + This test ensures the custom command functionality receiving a duplicate error response + without providing a `transaction_id`. + """ + + out = StringIO() + + for transaction in TransactionFactory.create_batch(10): + TransactionItemFactory.create(transaction=transaction) + SageX3TransactionInformationFactory.create( + transaction=transaction, status=SageX3TransactionInformation.FAILED + ) + + for transaction in TransactionFactory.create_batch(10): + TransactionItemFactory.create(transaction=transaction) + SageX3TransactionInformationFactory.create( + transaction=transaction, status=SageX3TransactionInformation.PENDING + ) + + call_command("retry_sage_transactions", stdout=out) - # test a failed transaction should be retried - f = create_transaction(SageX3TransactionInformation.FAILED) - call_command("retry_sage_transactions") - transaction_service_mock.assert_called_with(f) + message = "----- 20 Transactions were retried -----\n\nSUCCESSFULL RETRIES: 20 FAILED RETRIES: 0" - # test the force retry of a success transaction - t = create_transaction(SageX3TransactionInformation.SUCCESS) - call_command("retry_sage_transactions", transaction_id=t.transaction_id) - transaction_service_mock.assert_called_with(t) + self.assertTrue(message in out.getvalue()) diff --git a/apps/billing/tests/test_transaction_service.py b/apps/billing/tests/test_transaction_service.py index 6906c1c..b2bf739 100644 --- a/apps/billing/tests/test_transaction_service.py +++ b/apps/billing/tests/test_transaction_service.py @@ -4,7 +4,7 @@ from django.test.testcases import TestCase from requests.exceptions import Timeout -from apps.billing.factories import TransactionFactory, TransactionItemFactory +from apps.billing.factories import SageX3TransactionInformationFactory, TransactionFactory, TransactionItemFactory from apps.billing.mocks import MockResponse from apps.billing.models import SageX3TransactionInformation from apps.billing.services.transaction_service import TransactionService @@ -239,3 +239,416 @@ def test_transaction_to_processor_log_xml(self, mocked_post): log_message_output = cm.output[1] self.assertIn("Receiving from SageX3 the response", log_message_output) self.assertIn("