From 0848c032fcd892e09d91eb41d57d3f0fd3b784f4 Mon Sep 17 00:00:00 2001 From: Tiago-da-Silva23 Date: Fri, 27 Oct 2023 17:41:49 +0100 Subject: [PATCH 1/7] fix: adjusted the product_id parameter --- apps/shared_revenue/factories.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/shared_revenue/factories.py b/apps/shared_revenue/factories.py index d32d9e1..0543657 100644 --- a/apps/shared_revenue/factories.py +++ b/apps/shared_revenue/factories.py @@ -1,6 +1,3 @@ -import random -import string - import factory from django.utils import timezone from factory.django import DjangoModelFactory @@ -14,8 +11,9 @@ class Meta: organization = None partner_percentage = 0.70 - product_id = ( - f"course-v1:UPorto+CBN{random.choice(string.ascii_uppercase)}{random.choice(string.ascii_uppercase)}F+2023_T3" + product_id = factory.Faker( + "pystr_format", + string_format="course-v%:?????+CBN???F+2023_T3", ) start_date = factory.Faker( "date_time_between", start_date="-10d", end_date="+30d", tzinfo=timezone.get_current_timezone() From ad42244b2dcfa99b377e0e508de2a5db23de361b Mon Sep 17 00:00:00 2001 From: Tiago-da-Silva23 Date: Fri, 27 Oct 2023 17:42:46 +0100 Subject: [PATCH 2/7] fix: adjusted the check_each_configuration method --- apps/shared_revenue/models.py | 29 +++-------------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/apps/shared_revenue/models.py b/apps/shared_revenue/models.py index 04e2341..1cabbbc 100644 --- a/apps/shared_revenue/models.py +++ b/apps/shared_revenue/models.py @@ -125,36 +125,13 @@ def has_concurrent_revenue_configuration(self) -> bool: assert self.check_each_configuration(configuration=configuration) return False - except Exception: + except Exception as e: + e raise ValidationError("There is a concurrent revenue configuration in this moment") - def _check_partner_percentage(self) -> None: - try: - same_configurations: list[RevenueConfiguration] = RevenueConfiguration.objects.filter( - **{ - "organization": self.organization, - "product_id": self.product_id, - } - ) - - percentages = self.partner_percentage - for configuration in same_configurations: - if configuration.id == self.id: - continue - - percentages = percentages + configuration.partner_percentage - - assert percentages <= 1 - except Exception: - raise ValidationError("The partner percentage exceeds 100%") - def validate_instance(self) -> None: try: - validations = [ - self.has_concurrent_revenue_configuration, - self._check_partner_percentage, - ] - + validations = [self.has_concurrent_revenue_configuration] for validation in validations: validation() except Exception as e: From e8f1c67d687ff7e653578da3eed05aa65c7cf852 Mon Sep 17 00:00:00 2001 From: Tiago-da-Silva23 Date: Fri, 27 Oct 2023 17:44:12 +0100 Subject: [PATCH 3/7] feat: created tests for each possible situation of the RevenueConfiguration model --- .../tests/test_revenue_configurations.py | 298 +++++++++++++----- 1 file changed, 211 insertions(+), 87 deletions(-) diff --git a/apps/shared_revenue/tests/test_revenue_configurations.py b/apps/shared_revenue/tests/test_revenue_configurations.py index 540679b..bfd8a15 100644 --- a/apps/shared_revenue/tests/test_revenue_configurations.py +++ b/apps/shared_revenue/tests/test_revenue_configurations.py @@ -1,9 +1,14 @@ +from datetime import datetime, timedelta + +import factory from django.core.exceptions import ValidationError -from django.db.utils import IntegrityError +from django.db import IntegrityError from django.test import TestCase +from django.utils import timezone from apps.organization.factories import OrganizationFactory from apps.shared_revenue.factories import RevenueConfigurationFactory +from apps.shared_revenue.models import RevenueConfiguration class RevenueConfigurationTestCase(TestCase): @@ -13,130 +18,249 @@ def setUp(self): def test_str_method(self): """ - Test the `__str__` method of the `RevenueConfiguration` model. + Should validate that the `RevenueConfiguration.__str__` method returns the correct parameters in the correct sequence. """ + self.assertEqual( - str(self.revenue_configuration), f"{self.organization} - {self.revenue_configuration.product_code}" + str(self.revenue_configuration), + f"{self.organization} - {self.revenue_configuration.product_id} - {self.revenue_configuration.partner_percentage}", ) - def test_organization_or_product_code_without_course_or_organization(self): + def test_valid_partner_percentage(self): """ - Test that attempting to create a `RevenueConfiguration` instance without an `organization` - or `product_code` raises an `IntegrityError`. + Should validate that the `partner_percentage` is between 0 and 1. """ - with self.assertRaises(IntegrityError): - RevenueConfigurationFactory(organization=None, product_code=None) - def test_organization_and_product_code_can_not_be_both_filled(self): + self.assertGreaterEqual(self.revenue_configuration.partner_percentage, 0) + self.assertLessEqual(self.revenue_configuration.partner_percentage, 1) + + def test_required_parameters(self): """ - Test that attempting to create a `RevenueConfiguration` instance with both an `organization` - and a `product_code` raises an `IntegrityError`. + Should raise an error of type `IntegrityError`, it validates the required parameters to save a `RevenueConfiguration`. """ with self.assertRaises(IntegrityError): - RevenueConfigurationFactory(organization=self.organization, product_code="ABC123") + RevenueConfigurationFactory( + partner_percentage=None, + product_id=None, + ) - def test_organization_null(self): + def test_has_concurrent_revenue_configuration_with_dates(self): """ - Test that attempting to create a `RevenueConfiguration` instance with a null `organization` - and null `product_code` raises a `ValidationError`. + Should raise an error of type `ValidationError`, it validates that + already exists a `RevenueConfiguration` for this `Organization` and `product_id` + for the concurrent period of time. """ - with self.assertRaises(ValidationError): - RevenueConfigurationFactory(product_code="ABC123") - def test_product_code_null(self): - """ - Test that attempting to create a `RevenueConfiguration` instance with a null `product_code` - and null `organization` raises a `ValidationError`. - """ - with self.assertRaises(ValidationError): - RevenueConfigurationFactory(organization=self.organization) + with self.assertRaisesMessage( + expected_exception=ValidationError, + expected_message="There is a concurrent revenue configuration in this moment", + ): + RevenueConfigurationFactory( + start_date=factory.Faker( + "date_time_between", start_date="+1d", end_date="+20d", tzinfo=timezone.get_current_timezone() + ), + end_date=factory.Faker( + "date_time_between", start_date="+30d", end_date="+69d", tzinfo=timezone.get_current_timezone() + ), + product_id=self.revenue_configuration.product_id, + organization=self.organization, + ) - def test_start_date_is_now(self): + def test_has_concurrent_revenue_configuration_with_just_start_date(self): """ - Test that the `start_date` field is automatically set to the current date and time when a - `RevenueConfiguration` instance is created. + Should raise an error of type `ValidationError`, it validates that + already exists a `RevenueConfiguration` for this `Organization` and `product_id` + for the concurrent period of time, considering that the attempt is to save a None `end_date`. """ - revenue_configuration = RevenueConfigurationFactory(organization=self.organization) - self.assertIsNotNone(revenue_configuration.start_date) - def test_end_date_null(self): - """ - Test that the `end_date` field can be null when creating a `RevenueConfiguration` instance. - """ - revenue_configuration = RevenueConfigurationFactory(organization=self.organization, end_date=None) - self.assertIsNone(revenue_configuration.end_date) + with self.assertRaisesMessage( + expected_exception=ValidationError, + expected_message="There is a concurrent revenue configuration in this moment", + ): + RevenueConfigurationFactory( + start_date=factory.Faker( + "date_time_between", start_date="+1d", end_date="+20d", tzinfo=timezone.get_current_timezone() + ), + end_date=None, + product_id=self.revenue_configuration.product_id, + organization=self.organization, + ) - def test_end_date_not_null(self): + def test_has_concurrent_revenue_configuration_with_just_end_date(self): """ - Test that the `end_date` field cannot be null when creating a `RevenueConfiguration` instance. + Should raise an error of type `ValidationError`, it validates that + already exists a `RevenueConfiguration` for this `Organization` and `product_id` + for concurrent period of time, considering that the attempt is to save a None `start_date`. """ - with self.assertRaises(IntegrityError): - RevenueConfigurationFactory(organization=self.organization, end_date=None) - def test_product_code_max_length(self): - """ - Test that attempting to create a `RevenueConfiguration` instance with a `product_code` - that exceeds the maximum length raises a `ValidationError`. - """ - long_product_code = "a" * 51 - with self.assertRaises(ValidationError): - RevenueConfigurationFactory(organization=self.organization, product_code=long_product_code) + with self.assertRaisesMessage( + expected_exception=ValidationError, + expected_message="There is a concurrent revenue configuration in this moment", + ): + RevenueConfigurationFactory( + start_date=None, + end_date=factory.Faker( + "date_time_between", start_date="+30d", end_date="+69d", tzinfo=timezone.get_current_timezone() + ), + product_id=self.revenue_configuration.product_id, + organization=self.organization, + ) - def test_product_code_blank(self): + def test_has_concurrent_revenue_configuration_with_no_dates(self): """ - Test that the `product_code` field can be blank when creating a `RevenueConfiguration` instance. + Should raise an error of type `ValidationError`, it validates that + already exists a `RevenueConfiguration` for this `Organization` and `product_id` + in the concurrent period of time, considering that the attempt is to save + as None the `start_date` and `end_date` parameters. """ - revenue_configuration = RevenueConfigurationFactory(organization=self.organization, product_code="") - self.assertEqual(revenue_configuration.product_code, "") - def test_product_code_and_organization_null(self): - """ - Test that attempting to create a `RevenueConfiguration` instance with both `product_code` - and `organization` null raises a `ValidationError`. - """ - with self.assertRaises(ValidationError): - RevenueConfigurationFactory(organization=None, product_code=None) + with self.assertRaisesMessage( + expected_exception=ValidationError, + expected_message="There is a concurrent revenue configuration in this moment", + ): + RevenueConfigurationFactory( + start_date=None, + end_date=None, + product_id=self.revenue_configuration.product_id, + organization=self.organization, + ) - def test_organization_foreign_key(self): + def test_has_no_concurrent_revenue_configuration(self): """ - Test that the `organization` field is a foreign key to the `Organization` model. + Should register a new `RevenueConfiguration`, it validades that this attempt + is to save a new configuration which will not impact on the current, or any other configuration. """ - revenue_configuration = RevenueConfigurationFactory(organization=self.organization) - self.assertEqual(revenue_configuration.organization, self.organization) - def test_organization_related_name(self): - """ - Test that the `related_name` argument for the `organization` field is set to `"revenue_organizations"`. - """ - self.assertIn(self.organization) + new_configuration = RevenueConfigurationFactory( + start_date=factory.Faker( + "date_time_between", start_date="+71d", end_date="+80d", tzinfo=timezone.get_current_timezone() + ), + end_date=factory.Faker( + "date_time_between", start_date="+81d", end_date="+90d", tzinfo=timezone.get_current_timezone() + ), + organization=self.organization, + product_id=self.revenue_configuration.product_id, + ) - def test_organization_or_product_code_constraint(self): - """ - Test that the `organization_or_product_code` constraint is enforced. - """ - with self.assertRaises(IntegrityError): - RevenueConfigurationFactory(organization=None, product_code=None) + self.assertNotEqual(new_configuration.id, self.revenue_configuration.id) + self.assertEqual(new_configuration.organization, self.revenue_configuration.organization) + self.assertEqual(new_configuration.product_id, self.revenue_configuration.product_id) + self.assertTrue(new_configuration.start_date != self.revenue_configuration.start_date) + self.assertTrue(new_configuration.end_date != self.revenue_configuration.end_date) - def test_end_date_equal_to_start_date(self): + def test_future_configuration_creation(self): """ - Test that the `end_date` field can be equal to the `start_date` field when - creating a `RevenueConfiguration` instance. + Should register a new `RevenueConfiguration`, it validades that this attempt + is to save a future configuration considering as None the `end_date`, which will not impact + on the current, or any other configuration. """ - revenue_configuration = RevenueConfigurationFactory( + + new_configuration = RevenueConfigurationFactory( + start_date=factory.Faker( + "date_time_between", start_date="+82d", end_date="+90d", tzinfo=timezone.get_current_timezone() + ), + end_date=None, organization=self.organization, - start_date="2022-01-01 00:00:00", - end_date="2022-01-01 00:00:00", + product_id=self.revenue_configuration.product_id, ) - self.assertEqual(revenue_configuration.start_date, revenue_configuration.end_date) + self.assertNotEqual(new_configuration.id, self.revenue_configuration.id) + self.assertEqual(new_configuration.organization, self.revenue_configuration.organization) + self.assertEqual(new_configuration.product_id, self.revenue_configuration.product_id) + self.assertTrue(new_configuration.start_date > self.revenue_configuration.start_date) + self.assertIsNone(new_configuration.end_date) - def test_end_date_greater_than_start_date(self): + def test_older_configuration_creation(self): """ - Test that the `end_date` field can be greater than the `start_date` field when - creating a `RevenueConfiguration` instance. + Should register a new `RevenueConfiguration`, it validades that this attempt + is to save a older configuration, which will not impact on the current, or any other configuration. """ - revenue_configuration = RevenueConfigurationFactory( + + new_configuration = RevenueConfigurationFactory( + start_date=factory.Faker( + "date_time_between", start_date="-10d", end_date="-5d", tzinfo=timezone.get_current_timezone() + ), + end_date=factory.Faker( + "date_time_between", start_date="-4d", end_date="-1d", tzinfo=timezone.get_current_timezone() + ), organization=self.organization, - start_date="2021-01-01 00:00:00", - end_date="2022-01-01 00:00:00", + product_id=self.revenue_configuration.product_id, ) - self.assertLess(revenue_configuration.start_date, revenue_configuration.end_date) + self.assertNotEqual(new_configuration.id, self.revenue_configuration.id) + self.assertEqual(new_configuration.organization, self.revenue_configuration.organization) + self.assertEqual(new_configuration.product_id, self.revenue_configuration.product_id) + self.assertTrue(new_configuration.start_date < self.revenue_configuration.start_date) + self.assertTrue(new_configuration.end_date < self.revenue_configuration.start_date) + + def test_edit_configuration(self): + """ + Should edit a `RevenueConfiguration`, it validades that this attempt + is to edit an existing configuration in a way that will not impact on the current, + or any other configuration. + """ + + new_start_date = datetime.now(tz=timezone.get_current_timezone()) - timedelta(days=3) + new_end_date = datetime.now(tz=timezone.get_current_timezone()) - timedelta(days=1) + + self.assertEqual(type(self.revenue_configuration.start_date), datetime) + self.assertEqual(type(self.revenue_configuration.end_date), datetime) + self.assertNotEqual(self.revenue_configuration.start_date, new_start_date) + self.assertNotEqual(self.revenue_configuration.end_date, new_end_date) + self.assertNotEqual(self.revenue_configuration.product_id, "new_product_id") + self.assertNotEqual(self.revenue_configuration.partner_percentage, 0.71) + + self.revenue_configuration.start_date = new_start_date + self.revenue_configuration.end_date = new_end_date + self.revenue_configuration.partner_percentage = 0.71 + self.revenue_configuration.product_id = "new_product_id" + + self.assertEqual(type(self.revenue_configuration.start_date), datetime) + self.assertEqual(type(self.revenue_configuration.end_date), datetime) + self.assertEqual(self.revenue_configuration.start_date, new_start_date) + self.assertEqual(self.revenue_configuration.end_date, new_end_date) + self.assertEqual(self.revenue_configuration.partner_percentage, 0.71) + self.assertEqual(self.revenue_configuration.product_id, "new_product_id") + self.assertEqual(type(self.revenue_configuration), RevenueConfiguration) + self.revenue_configuration.save() + + def test_edit_has_concurrent_revenue_configuration(self): + """ + Should raise an error of type `ValidationError`, it validades that is not possible to edit + an existing configuration in a way that will impact on the current, or any other configuration. + """ + + with self.assertRaisesMessage( + expected_exception=ValidationError, + expected_message="There is a concurrent revenue configuration in this moment", + ): + new_configuration = RevenueConfigurationFactory( + start_date=factory.Faker( + "date_time_between", start_date="-10d", end_date="-5d", tzinfo=timezone.get_current_timezone() + ), + end_date=factory.Faker( + "date_time_between", start_date="-4d", end_date="-1d", tzinfo=timezone.get_current_timezone() + ), + organization=self.organization, + product_id=self.revenue_configuration.product_id, + ) + + new_start_date = new_configuration.start_date + timedelta(days=1) + new_end_date = new_configuration.end_date - timedelta(days=1) + + self.assertEqual(type(new_start_date), datetime) + self.assertEqual(type(new_end_date), datetime) + self.assertEqual(type(new_configuration.start_date), datetime) + self.assertEqual(type(new_configuration.end_date), datetime) + self.assertEqual(type(self.revenue_configuration.start_date), datetime) + self.assertEqual(type(self.revenue_configuration.end_date), datetime) + self.assertEqual(type(new_configuration), RevenueConfiguration) + + self.assertNotEqual(self.revenue_configuration.start_date, new_start_date) + self.assertNotEqual(self.revenue_configuration.end_date, new_end_date) + self.assertNotEqual(new_configuration.start_date, self.revenue_configuration.start_date) + self.assertNotEqual(new_configuration.end_date, self.revenue_configuration.end_date) + + self.revenue_configuration.start_date = new_start_date + self.revenue_configuration.end_date = new_end_date + + self.assertEqual(type(self.revenue_configuration), RevenueConfiguration) + self.revenue_configuration.save() From f07266b587c6bf270710b39a562f8882475c9b90 Mon Sep 17 00:00:00 2001 From: Tiago-da-Silva23 Date: Tue, 31 Oct 2023 14:20:33 +0000 Subject: [PATCH 4/7] test: created test to validate split result and split_result instance --- .../services/split_execution.py | 1 + .../tests/test_split_execution.py | 86 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 apps/shared_revenue/tests/test_split_execution.py diff --git a/apps/shared_revenue/services/split_execution.py b/apps/shared_revenue/services/split_execution.py index 2b65ae9..26da8f8 100644 --- a/apps/shared_revenue/services/split_execution.py +++ b/apps/shared_revenue/services/split_execution.py @@ -141,6 +141,7 @@ def _calculate_transactions( split_results: list[Dict] = [] used_configurations: list[Dict] = [] + # calculate NAU percentage base on the sum of all partner percentages per product_id for item in transaction_items: for configuration in configurations: if item.organization_code == configuration.organization.short_name: diff --git a/apps/shared_revenue/tests/test_split_execution.py b/apps/shared_revenue/tests/test_split_execution.py new file mode 100644 index 0000000..9304247 --- /dev/null +++ b/apps/shared_revenue/tests/test_split_execution.py @@ -0,0 +1,86 @@ +from datetime import datetime, timedelta +from decimal import Decimal + +from django.test import TestCase + +from apps.billing.factories import Transaction, TransactionFactory, TransactionItem, TransactionItemFactory +from apps.organization.factories import Organization, OrganizationFactory +from apps.shared_revenue.factories import RevenueConfigurationFactory +from apps.shared_revenue.services.split_execution import SplitExecutionService, SplitResult + + +class SplitExecutionServiceTestCase(TestCase): + def setUp(self) -> None: + self.nau_percentage = round(Decimal(0.30), 2) + self.partner_percentage = round(Decimal(0.70), 2) + + self.organizations: list[Organization] = OrganizationFactory.create_batch(5) + self.transactions: list[Transaction] = [] + self.transaction_items: list[TransactionItem] = [] + + for organization in self.organizations: + transaction = TransactionFactory.create() + self.transactions.append(transaction) + + item = TransactionItemFactory.create( + organization_code=organization.short_name, + transaction=transaction, + ) + self.transaction_items.append(item) + + RevenueConfigurationFactory.create( + organization=organization, + product_id=item.product_id, + partner_percentage=self.partner_percentage, + ) + + self.columns = [ + "product_name", + "transaction_date", + "total_amount_include_vat", + "total_amount_exclude_vat", + "organization_code", + "amount_for_nau", + "amount_for_organization", + ] + + self.split_result: SplitResult = SplitExecutionService( + start_date=datetime.now() - timedelta(days=1), + end_date=datetime.now() + timedelta(days=2), + ).execute_split_steps() + + def test_validate_the_split_value_result(self): + + for split_result in self.split_result.results: + self.assertEqual( + split_result["amount_for_nau"], split_result["total_amount_include_vat"] * self.nau_percentage + ) + self.assertEqual( + split_result["amount_for_organization"], + split_result["total_amount_include_vat"] * self.partner_percentage, + ) + + def test_split_result_instance(self): + self.assertEqual(self.split_result.file_name, "test_file") + for split_result in self.split_result.results: + self.assertEqual(self.columns, list(split_result.keys())) + # + # self.assertEqual(self.split_result.columns, self.columns) + + def test_filter_transaction_items(self): + pass + + def test_filter_revenue_configurations(self): + pass + + def test_assembly_each_result(self): + pass + + def test_calculate_transactions(self): + pass + + def test_execute_split_steps(self): + pass + + def tearDown(self) -> None: + return super().tearDown() From 91d7c72bd817a987984ffac62ac02ff6eff20964 Mon Sep 17 00:00:00 2001 From: Tiago-da-Silva23 Date: Wed, 8 Nov 2023 17:43:02 +0000 Subject: [PATCH 5/7] refactor: removed unnecessary comment --- apps/shared_revenue/services/split_execution.py | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/shared_revenue/services/split_execution.py b/apps/shared_revenue/services/split_execution.py index 26da8f8..2b65ae9 100644 --- a/apps/shared_revenue/services/split_execution.py +++ b/apps/shared_revenue/services/split_execution.py @@ -141,7 +141,6 @@ def _calculate_transactions( split_results: list[Dict] = [] used_configurations: list[Dict] = [] - # calculate NAU percentage base on the sum of all partner percentages per product_id for item in transaction_items: for configuration in configurations: if item.organization_code == configuration.organization.short_name: From 99194a6ed354e0607e2c446ae872cb00a17d18d6 Mon Sep 17 00:00:00 2001 From: Tiago-da-Silva23 Date: Mon, 13 Nov 2023 12:29:56 +0000 Subject: [PATCH 6/7] feat: created tests for shared_revenue, fix: adjusted billing factories parameters --- apps/billing/factories.py | 10 +- apps/shared_revenue/models.py | 26 ++- apps/shared_revenue/tests/__init__.py | 0 .../tests/test_revenue_configurations.py | 72 ++++-- .../tests/test_split_execution.py | 220 ++++++++++++++---- 5 files changed, 253 insertions(+), 75 deletions(-) create mode 100644 apps/shared_revenue/tests/__init__.py diff --git a/apps/billing/factories.py b/apps/billing/factories.py index b6b6bba..548bd6d 100644 --- a/apps/billing/factories.py +++ b/apps/billing/factories.py @@ -30,7 +30,7 @@ class Meta: payment_type = factory.fuzzy.FuzzyChoice(PAYMENT_TYPE) transaction_type = factory.Faker("random_element", elements=[t[0] for t in TRANSACTION_TYPE]) transaction_date = factory.Faker( - "date_time_between", start_date="-1d", end_date="-5d", tzinfo=timezone.get_current_timezone() + "date_time_between", start_date="-5d", end_date="-1d", tzinfo=timezone.get_current_timezone() ) document_id = factory.Faker("pystr_format", string_format="DCI-######{{random_int}}") @@ -48,7 +48,6 @@ class Meta: description = factory.Faker("sentence") quantity = factory.Faker("pyint", min_value=1, max_value=10) vat_tax = factory.Faker("pydecimal", min_value=1, max_value=100, left_digits=3, right_digits=2) - amount_exclude_vat = factory.Faker("pydecimal", min_value=1, max_value=100, left_digits=5, right_digits=2) organization_code = factory.LazyAttribute(lambda obj: slugify(obj.description)) product_code = "".join([random.choice(string.ascii_uppercase) for _ in range(5)]) @@ -56,7 +55,10 @@ class Meta: def product_id(self): return f"course-v1:{self.organization_code}+{self.product_code}+2023_T3" - # Assuming 20% VAT + @factory.lazy_attribute + def amount_exclude_vat(self): + return self.transaction.total_amount_exclude_vat + @factory.lazy_attribute def amount_include_vat(self): - return round(self.amount_exclude_vat * Decimal("1.20"), 2) + return round(self.transaction.total_amount_exclude_vat * Decimal("1.20"), 2) diff --git a/apps/shared_revenue/models.py b/apps/shared_revenue/models.py index 1cabbbc..98d89fb 100644 --- a/apps/shared_revenue/models.py +++ b/apps/shared_revenue/models.py @@ -125,13 +125,33 @@ def has_concurrent_revenue_configuration(self) -> bool: assert self.check_each_configuration(configuration=configuration) return False - except Exception as e: - e + except Exception: raise ValidationError("There is a concurrent revenue configuration in this moment") + def _check_partner_percentage(self) -> None: + try: + same_configurations: list[RevenueConfiguration] = RevenueConfiguration.objects.filter( + product_id=self.product_id + ) + + percentages = self.partner_percentage + for configuration in same_configurations: + if configuration.id == self.id: + continue + + percentages = percentages + configuration.partner_percentage + + assert percentages <= 1 + except Exception: + raise ValidationError("The partner percentage exceeds 100%") + def validate_instance(self) -> None: try: - validations = [self.has_concurrent_revenue_configuration] + validations = [ + self.has_concurrent_revenue_configuration, + self._check_partner_percentage, + ] + for validation in validations: validation() except Exception as e: diff --git a/apps/shared_revenue/tests/__init__.py b/apps/shared_revenue/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/shared_revenue/tests/test_revenue_configurations.py b/apps/shared_revenue/tests/test_revenue_configurations.py index bfd8a15..bc14161 100644 --- a/apps/shared_revenue/tests/test_revenue_configurations.py +++ b/apps/shared_revenue/tests/test_revenue_configurations.py @@ -1,8 +1,8 @@ from datetime import datetime, timedelta +from decimal import Decimal import factory from django.core.exceptions import ValidationError -from django.db import IntegrityError from django.test import TestCase from django.utils import timezone @@ -34,16 +34,6 @@ def test_valid_partner_percentage(self): self.assertGreaterEqual(self.revenue_configuration.partner_percentage, 0) self.assertLessEqual(self.revenue_configuration.partner_percentage, 1) - def test_required_parameters(self): - """ - Should raise an error of type `IntegrityError`, it validates the required parameters to save a `RevenueConfiguration`. - """ - with self.assertRaises(IntegrityError): - RevenueConfigurationFactory( - partner_percentage=None, - product_id=None, - ) - def test_has_concurrent_revenue_configuration_with_dates(self): """ Should raise an error of type `ValidationError`, it validates that @@ -64,6 +54,7 @@ def test_has_concurrent_revenue_configuration_with_dates(self): ), product_id=self.revenue_configuration.product_id, organization=self.organization, + partner_percentage=0, ) def test_has_concurrent_revenue_configuration_with_just_start_date(self): @@ -84,6 +75,7 @@ def test_has_concurrent_revenue_configuration_with_just_start_date(self): end_date=None, product_id=self.revenue_configuration.product_id, organization=self.organization, + partner_percentage=0, ) def test_has_concurrent_revenue_configuration_with_just_end_date(self): @@ -104,6 +96,7 @@ def test_has_concurrent_revenue_configuration_with_just_end_date(self): ), product_id=self.revenue_configuration.product_id, organization=self.organization, + partner_percentage=0, ) def test_has_concurrent_revenue_configuration_with_no_dates(self): @@ -123,6 +116,7 @@ def test_has_concurrent_revenue_configuration_with_no_dates(self): end_date=None, product_id=self.revenue_configuration.product_id, organization=self.organization, + partner_percentage=0, ) def test_has_no_concurrent_revenue_configuration(self): @@ -140,6 +134,7 @@ def test_has_no_concurrent_revenue_configuration(self): ), organization=self.organization, product_id=self.revenue_configuration.product_id, + partner_percentage=0, ) self.assertNotEqual(new_configuration.id, self.revenue_configuration.id) @@ -153,6 +148,12 @@ def test_future_configuration_creation(self): Should register a new `RevenueConfiguration`, it validades that this attempt is to save a future configuration considering as None the `end_date`, which will not impact on the current, or any other configuration. + + In the last line of this test function, the new configuration is deleted, + the reason for that is because leaving this configuration created, it will + prevent the creation of another new one and break the other tests. This behavior + is right, as expected it is not possible to create another one, if there are a concurrent + configuration, then it's deleted because the testing needs to continue. """ new_configuration = RevenueConfigurationFactory( @@ -162,12 +163,14 @@ def test_future_configuration_creation(self): end_date=None, organization=self.organization, product_id=self.revenue_configuration.product_id, + partner_percentage=0, ) self.assertNotEqual(new_configuration.id, self.revenue_configuration.id) self.assertEqual(new_configuration.organization, self.revenue_configuration.organization) self.assertEqual(new_configuration.product_id, self.revenue_configuration.product_id) self.assertTrue(new_configuration.start_date > self.revenue_configuration.start_date) self.assertIsNone(new_configuration.end_date) + new_configuration.delete() def test_older_configuration_creation(self): """ @@ -177,13 +180,14 @@ def test_older_configuration_creation(self): new_configuration = RevenueConfigurationFactory( start_date=factory.Faker( - "date_time_between", start_date="-10d", end_date="-5d", tzinfo=timezone.get_current_timezone() + "date_time_between", start_date="-100d", end_date="-98d", tzinfo=timezone.get_current_timezone() ), end_date=factory.Faker( - "date_time_between", start_date="-4d", end_date="-1d", tzinfo=timezone.get_current_timezone() + "date_time_between", start_date="-97d", end_date="-95d", tzinfo=timezone.get_current_timezone() ), organization=self.organization, product_id=self.revenue_configuration.product_id, + partner_percentage=0, ) self.assertNotEqual(new_configuration.id, self.revenue_configuration.id) self.assertEqual(new_configuration.organization, self.revenue_configuration.organization) @@ -206,21 +210,20 @@ def test_edit_configuration(self): self.assertNotEqual(self.revenue_configuration.start_date, new_start_date) self.assertNotEqual(self.revenue_configuration.end_date, new_end_date) self.assertNotEqual(self.revenue_configuration.product_id, "new_product_id") - self.assertNotEqual(self.revenue_configuration.partner_percentage, 0.71) + self.assertNotEqual(self.revenue_configuration.partner_percentage, 0.69) self.revenue_configuration.start_date = new_start_date self.revenue_configuration.end_date = new_end_date - self.revenue_configuration.partner_percentage = 0.71 + self.revenue_configuration.partner_percentage = 0.69 self.revenue_configuration.product_id = "new_product_id" self.assertEqual(type(self.revenue_configuration.start_date), datetime) self.assertEqual(type(self.revenue_configuration.end_date), datetime) self.assertEqual(self.revenue_configuration.start_date, new_start_date) self.assertEqual(self.revenue_configuration.end_date, new_end_date) - self.assertEqual(self.revenue_configuration.partner_percentage, 0.71) + self.assertEqual(self.revenue_configuration.partner_percentage, 0.69) self.assertEqual(self.revenue_configuration.product_id, "new_product_id") self.assertEqual(type(self.revenue_configuration), RevenueConfiguration) - self.revenue_configuration.save() def test_edit_has_concurrent_revenue_configuration(self): """ @@ -241,6 +244,7 @@ def test_edit_has_concurrent_revenue_configuration(self): ), organization=self.organization, product_id=self.revenue_configuration.product_id, + partner_percentage=0, ) new_start_date = new_configuration.start_date + timedelta(days=1) @@ -264,3 +268,37 @@ def test_edit_has_concurrent_revenue_configuration(self): self.assertEqual(type(self.revenue_configuration), RevenueConfiguration) self.revenue_configuration.save() + + def test_check_partner_percentage_with_error(self): + """ + Should raise an error of type `ValidationError`, it validades that is not possible to create + a configuration that exceeds 100% of partners percentage + """ + + with self.assertRaisesMessage( + expected_exception=ValidationError, + expected_message="The partner percentage exceeds 100%", + ): + self.revenue_configuration.partner_percentage = Decimal(0.7) + self.revenue_configuration.save() + + RevenueConfigurationFactory( + product_id=self.revenue_configuration.product_id, + partner_percentage=Decimal(0.31), + ) + + def test_check_partner_percentage_with_success(self): + """ + Should create a new configuration, it validates that is + possible to register a new configuration when does not exceed + 100% in the `partner_percentage`'s calculation. + """ + + self.revenue_configuration.partner_percentage = Decimal(0.71) + self.revenue_configuration.save() + + new_configuration = RevenueConfigurationFactory( + product_id=self.revenue_configuration.product_id, + partner_percentage=Decimal(0.29), + ) + new_configuration.delete() diff --git a/apps/shared_revenue/tests/test_split_execution.py b/apps/shared_revenue/tests/test_split_execution.py index 9304247..0b2a575 100644 --- a/apps/shared_revenue/tests/test_split_execution.py +++ b/apps/shared_revenue/tests/test_split_execution.py @@ -1,86 +1,204 @@ from datetime import datetime, timedelta from decimal import Decimal +from typing import Callable +import factory from django.test import TestCase +from django.utils import timezone from apps.billing.factories import Transaction, TransactionFactory, TransactionItem, TransactionItemFactory from apps.organization.factories import Organization, OrganizationFactory from apps.shared_revenue.factories import RevenueConfigurationFactory -from apps.shared_revenue.services.split_execution import SplitExecutionService, SplitResult +from apps.shared_revenue.models import RevenueConfiguration +from apps.shared_revenue.services.split_execution import SplitExecutionService class SplitExecutionServiceTestCase(TestCase): def setUp(self) -> None: + """ + Defines and start the entire test base. + + - nau percentage + - partner_percentage + - list of organizations + - list of transactions + - list of transaction items + - list of configurations + - start and end dates parameters + """ + self.nau_percentage = round(Decimal(0.30), 2) self.partner_percentage = round(Decimal(0.70), 2) self.organizations: list[Organization] = OrganizationFactory.create_batch(5) self.transactions: list[Transaction] = [] self.transaction_items: list[TransactionItem] = [] + self.configurations: list[RevenueConfiguration] = [] for organization in self.organizations: - transaction = TransactionFactory.create() - self.transactions.append(transaction) + generated_transactions = TransactionFactory.create_batch(5) + self.transactions += generated_transactions - item = TransactionItemFactory.create( - organization_code=organization.short_name, - transaction=transaction, - ) - self.transaction_items.append(item) + for transaction in generated_transactions: + item = TransactionItemFactory.create( + organization_code=organization.short_name, + transaction=transaction, + ) + self.transaction_items.append(item) - RevenueConfigurationFactory.create( + configuration = RevenueConfigurationFactory.create( organization=organization, product_id=item.product_id, partner_percentage=self.partner_percentage, + start_date=factory.Faker( + "date_time_between", + start_date="-6d", + end_date="+1d", + tzinfo=timezone.get_current_timezone(), + ), ) + self.configurations.append(configuration) + + start_date = str((datetime.now() - timedelta(days=11)).date()).replace("-", "/") + end_date = str((datetime.now() + timedelta(days=30)).date()).replace("-", "/") + + start_date = datetime.strptime(start_date, "%Y/%m/%d").isoformat() + end_date = ( + datetime.strptime(end_date, "%Y/%m/%d") + (timedelta(days=1) - timedelta(milliseconds=1)) + ).isoformat() + + self.options = { + "start_date": start_date, + "end_date": end_date, + } + + def test_execute_split_without_filters(self): + """ + Should validate that it is possible calculate all the transactions + based on the given dates. + """ + + split_result = SplitExecutionService( + start_date=self.options["start_date"], + end_date=self.options["end_date"], + ).execute_split_steps() - self.columns = [ - "product_name", - "transaction_date", - "total_amount_include_vat", - "total_amount_exclude_vat", - "organization_code", - "amount_for_nau", - "amount_for_organization", - ] - - self.split_result: SplitResult = SplitExecutionService( - start_date=datetime.now() - timedelta(days=1), - end_date=datetime.now() + timedelta(days=2), + results = split_result[0] + configurations = split_result[1] + + self.assertTrue(len(results) > 0) + self.assertTrue(len(configurations) > 0) + + def test_filter_by_organization(self): + """ + Should validate that it is possible calculate all the transactions based on + the given dates by filtering for only transactions of certain organization. + """ + + kwargs = {"organization_code": self.organizations[0].short_name} + split_result = SplitExecutionService( + start_date=self.options["start_date"], + end_date=self.options["end_date"], + ).execute_split_steps(**kwargs) + + results = split_result[0] + configurations = split_result[1] + + self.assertTrue(len(results) > 0) + self.assertTrue(len(configurations) > 0) + + for result in results: + self.assertEqual(result["organization"], self.organizations[0].short_name) + + for configuration in configurations: + self.assertEqual(configuration["organization"], self.organizations[0].short_name) + + def test_filter_by_product(self): + """ + Should validate that it is possible calculate all the transactions based on + the given dates by filtering for only transactions of certain product. + """ + + kwargs = {"product_id": self.transaction_items[0].product_id} + split_result = SplitExecutionService( + start_date=self.options["start_date"], + end_date=self.options["end_date"], + ).execute_split_steps(**kwargs) + + results = split_result[0] + configurations = split_result[1] + + for result in results: + self.assertEqual(result["product_id"], self.transaction_items[0].product_id) + + for configuration in configurations: + self.assertEqual(configuration["product_id"], self.transaction_items[0].product_id) + + def test_filter_by_organization_and_product(self): + """ + Should validate that it is possible calculate all the transactions based on + the given dates by filtering for only transactions of certain combined organization and product. + """ + + kwargs = { + "organization_code": self.organizations[1].short_name, + "product_id": self.transaction_items[1].product_id, + } + split_result = SplitExecutionService( + start_date=self.options["start_date"], + end_date=self.options["end_date"], + ).execute_split_steps(**kwargs) + + results = split_result[0] + configurations = split_result[1] + + for result in results: + self.assertEqual(result["product_id"], self.transaction_items[1].product_id) + self.assertEqual(result["organization"], self.organizations[1].short_name) + + for configuration in configurations: + self.assertEqual(configuration["product_id"], self.transaction_items[1].product_id) + self.assertEqual(result["organization"], self.organizations[1].short_name) + + def test_calculate_transactions_results(self): + """ + Should validate the calculated result of each transaction based on the defined + parameters in the `setUp` method, every result should respect the expected value + """ + split_result = SplitExecutionService( + start_date=self.options["start_date"], + end_date=self.options["end_date"], ).execute_split_steps() - def test_validate_the_split_value_result(self): + results = split_result[0] + configurations = split_result[1] + + self.assertTrue(len(results) > 0) + self.assertTrue(len(configurations) > 0) + + for result in results: + current_item: Callable[[TransactionItem], bool] = lambda i: [ + i.product_id, + i.organization_code, + i.transaction.transaction_id, + ] == [ + result["product_id"], + result["organization"], + result["transaction_id"], + ] + + item = [i for i in self.transaction_items if current_item(i=i)][0] - for split_result in self.split_result.results: + self.assertEqual(result["percentage_for_organization"], self.partner_percentage) self.assertEqual( - split_result["amount_for_nau"], split_result["total_amount_include_vat"] * self.nau_percentage + result["amount_for_organization_including_vat"], + item.amount_include_vat * self.partner_percentage, ) self.assertEqual( - split_result["amount_for_organization"], - split_result["total_amount_include_vat"] * self.partner_percentage, + result["amount_for_organization_exclude_vat"], + item.amount_exclude_vat * self.partner_percentage, ) - def test_split_result_instance(self): - self.assertEqual(self.split_result.file_name, "test_file") - for split_result in self.split_result.results: - self.assertEqual(self.columns, list(split_result.keys())) - # - # self.assertEqual(self.split_result.columns, self.columns) - - def test_filter_transaction_items(self): - pass - - def test_filter_revenue_configurations(self): - pass - - def test_assembly_each_result(self): - pass - - def test_calculate_transactions(self): - pass - - def test_execute_split_steps(self): - pass - - def tearDown(self) -> None: - return super().tearDown() + self.assertEqual(result["percentage_for_nau"], self.nau_percentage) + self.assertEqual(result["amount_for_nau_including_vat"], item.amount_include_vat * self.nau_percentage) + self.assertEqual(result["amount_for_nau_exclude_vat"], item.amount_exclude_vat * self.nau_percentage) From 0cb1cea7b1541b406581249bf9d71c8364fb5d57 Mon Sep 17 00:00:00 2001 From: Tiago-da-Silva23 Date: Mon, 13 Nov 2023 15:16:51 +0000 Subject: [PATCH 7/7] fix: adjusted pointed out errors in the PR --- apps/shared_revenue/tests/test_split_execution.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/shared_revenue/tests/test_split_execution.py b/apps/shared_revenue/tests/test_split_execution.py index 0b2a575..052b2e2 100644 --- a/apps/shared_revenue/tests/test_split_execution.py +++ b/apps/shared_revenue/tests/test_split_execution.py @@ -1,6 +1,6 @@ +from collections.abc import Callable from datetime import datetime, timedelta from decimal import Decimal -from typing import Callable import factory from django.test import TestCase @@ -74,7 +74,7 @@ def setUp(self) -> None: def test_execute_split_without_filters(self): """ - Should validate that it is possible calculate all the transactions + Should validate that it is possible to calculate all the transactions based on the given dates. """ @@ -91,8 +91,8 @@ def test_execute_split_without_filters(self): def test_filter_by_organization(self): """ - Should validate that it is possible calculate all the transactions based on - the given dates by filtering for only transactions of certain organization. + Should validate that it is possible to calculate all the transactions based on + the given dates by filtering for only transactions of a certain organization. """ kwargs = {"organization_code": self.organizations[0].short_name} @@ -115,8 +115,8 @@ def test_filter_by_organization(self): def test_filter_by_product(self): """ - Should validate that it is possible calculate all the transactions based on - the given dates by filtering for only transactions of certain product. + Should validate that it is possible to calculate all the transactions based on + the given dates by filtering for only transactions of a certain product. """ kwargs = {"product_id": self.transaction_items[0].product_id} @@ -136,7 +136,7 @@ def test_filter_by_product(self): def test_filter_by_organization_and_product(self): """ - Should validate that it is possible calculate all the transactions based on + Should validate that it is possible to calculate all the transactions based on the given dates by filtering for only transactions of certain combined organization and product. """