Skip to content

Commit

Permalink
Merge pull request #155 from fccn/74-increase-billing-tests-coverage
Browse files Browse the repository at this point in the history
74 increase billing tests coverage
  • Loading branch information
jamoqs authored Nov 2, 2023
2 parents 56ef048 + 2ecc5ee commit 423b215
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 73 deletions.
10 changes: 5 additions & 5 deletions apps/billing/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Meta:

transaction_id = factory.Faker(
"pystr_format",
string_format="NAU-######{random_int}",
string_format="NAU-######{{random_int}}",
)
client_name = factory.Faker("name")
email = factory.Faker("email")
Expand All @@ -26,16 +26,16 @@ class Meta:
vat_identification_number = factory.Faker("ssn")
total_amount_exclude_vat = factory.Faker("pydecimal", min_value=1, max_value=100, left_digits=5, right_digits=2)
payment_type = factory.fuzzy.FuzzyChoice(PAYMENT_TYPE)
transaction_type = factory.fuzzy.FuzzyChoice(TRANSACTION_TYPE[0])
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()
)
document_id = factory.Faker("pystr_format", string_format="DCI-######{random_int}")
document_id = factory.Faker("pystr_format", string_format="DCI-######{{random_int}}")

# Assuming 20% VAT
@factory.lazy_attribute
def total_amount_include_vat(self):
return self.total_amount_exclude_vat * Decimal("1.20")
return round(self.total_amount_exclude_vat * Decimal("1.20"), 2)


class TransactionItemFactory(factory.django.DjangoModelFactory):
Expand All @@ -54,4 +54,4 @@ class Meta:
# Assuming 20% VAT
@factory.lazy_attribute
def amount_include_vat(self):
return self.amount_exclude_vat * Decimal("1.20")
return round(self.amount_exclude_vat * Decimal("1.20"), 2)
94 changes: 55 additions & 39 deletions apps/billing/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from copy import deepcopy

from django_countries.serializers import CountryFieldMixin
from rest_framework import serializers

Expand Down Expand Up @@ -47,8 +45,27 @@ class Meta:
fields = "__all__"


class TransactionItemSerializerWithoutTransaction(TransactionItemSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
del self.fields["transaction"]


class ProcessTransactionSerializer(CountryFieldMixin, serializers.ModelSerializer):
item = TransactionItemSerializer()
"""
Serializer for processing transactions.
This serializer is responsible for validating and processing transaction data. It includes methods for creating
transactions, executing shared revenue resources, and converting transaction data to internal and external formats.
Methods:
_execute_shared_revenue_resources(organization: Organization, product_id: str): Checks if a revenue configuration
exists for the given organization and product ID, and creates one if it doesn't exist.
_execute_billing_resources(validate_data: dict): Creates a transaction and a transaction item from the given data.
create(validate_data): Creates a transaction, a transaction item, and a revenue configuration from the given data.
to_internal_value(data): Converts the given data to an internal format.
to_representation(instance): Returns the given instance.
"""

item = TransactionItemSerializerWithoutTransaction(required=True, allow_null=False)

class Meta:
model = Transaction
Expand Down Expand Up @@ -78,60 +95,59 @@ def _execute_shared_revenue_resources(
self,
organization: Organization,
product_id: str,
):
revenue_configuration_exists: bool = self._has_concurrent_revenue_configuration(
organization=organization,
product_id=product_id,
)
if not revenue_configuration_exists:
RevenueConfiguration.objects.create(**{"organization": organization, "product_id": product_id})

def _execute_billing_resources(
self,
validate_data: dict,
):
transaction_data = {k: v for k, v in validate_data.items() if k != "item"}
transaction = Transaction.objects.create(**transaction_data)

transaction_item_data = deepcopy(validate_data["item"])
transaction_item_data["transaction"] = transaction
item = TransactionItem.objects.create(**transaction_item_data)

return transaction, item

def _has_concurrent_revenue_configuration(
self,
organization: Organization,
product_id: str,
):
try:
return RevenueConfiguration(
revenue_configuration_exists = RevenueConfiguration(
organization=organization,
product_id=product_id,
).has_concurrent_revenue_configuration()
if not revenue_configuration_exists:
RevenueConfiguration.objects.create(**{"organization": organization, "product_id": product_id})
except Exception:
return True

def _execute_billing_resources(
self,
validate_data: dict,
):
item = validate_data.pop("item", None)
transaction = Transaction.objects.create(**validate_data)
item["transaction"] = transaction
item = TransactionItem.objects.create(**item)

return item

def create(self, validate_data):
try:
item = self._execute_billing_resources(validate_data=validate_data)
organization, created = Organization.objects.get_or_create(
short_name=validate_data["item"]["organization_code"],
defaults={"short_name": validate_data["item"]["organization_code"]},
short_name=item.organization_code,
defaults={"short_name": item.organization_code},
)
self._execute_shared_revenue_resources(
organization=organization,
product_id=validate_data["item"]["product_id"],
product_id=item.product_id,
)
return validate_data
except Exception as e:
raise e

def to_internal_value(self, data):
transaction, item = self._execute_billing_resources(validate_data=data)
data = TransactionSerializer(transaction).data
data["item"] = TransactionItemSerializer(item).data

return data

def to_representation(self, instance):
return instance

def validate(self, data):
serializer_fields = set(self.fields.keys())
data_fields = set(self.initial_data.keys())
extra_fields = data_fields - serializer_fields
if extra_fields:
raise serializers.ValidationError(f"Extra fields: {', '.join(extra_fields)}")

item = data.pop("item", None)
transaction = TransactionSerializer(data=data)
transaction.is_valid(raise_exception=True)
item = TransactionItemSerializerWithoutTransaction(data=item)
item.is_valid(raise_exception=True)
transaction = transaction.data
transaction["item"] = item.data

return transaction
122 changes: 93 additions & 29 deletions apps/billing/tests/test_process_transaction.py
Original file line number Diff line number Diff line change
@@ -1,58 +1,122 @@
import json

import factory
from django.contrib.auth import get_user_model
from django.test import TestCase
from rest_framework.authtoken.models import Token
from rest_framework.test import APIClient

from apps.billing.factories import TransactionFactory, TransactionItemFactory
from apps.billing.models import Transaction
from apps.billing.serializers import ProcessTransactionSerializer
from apps.billing.serializers import TransactionItemSerializer, TransactionSerializer


class ProcessTransactionTest(TestCase):
"""
A test case for the ProcessTransaction view.
"""

def setUp(self):
self.payload = {
"transaction_id": "a9k05000-227f-4500-b71f-9f00fba1cf5f",
"client_name": "Cliente name",
"email": "[email protected]",
"address_line_1": "Av. Liberdade",
"address_line_2": "",
"city": "Lisboa",
"postal_code": "1250-142",
"state": "",
"country_code": "PT",
"vat_identification_number": "PT220234835",
"vat_identification_country": "PT",
"total_amount_exclude_vat": 114.73,
"total_amount_include_vat": 149.00,
"currency": "EUR",
"item": {
"description": "The product/line text with a description of what have been bought. The field need to be a string.",
"quantity": 1,
"vat_tax": 114.73,
"amount_exclude_vat": 114.73,
"amount_include_vat": 149.00,
"organization_code": "UPorto",
"product_id": "course-v1:UPorto+CBNEEF+2023_T3",
"product_code": "CBNEEF",
},
}
"""
Set up the test case by creating a client, endpoint, transaction, transaction item, and payload.
Also create a new user and generate a token for that user.
"""
self.client = APIClient()
self.endpoint = "/api/billing/transaction-complete/"

self.transaction = factory.build(dict, FACTORY_CLASS=TransactionFactory)
self.transaction_item = factory.build(dict, FACTORY_CLASS=TransactionItemFactory, transaction=None)
self.transaction["item"] = self.transaction_item
self.payload = self.transaction

# Create a new user and generate a token for that user
self.user = get_user_model().objects.create_user(username="testuser", password="testpass")
self.token = Token.objects.create(user=self.user)

def test_create_transaction(self):
"""
Test that a transaction can be created with a valid token.
"""
self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key)

response = self.client.post(self.endpoint, self.payload, format="json")
self.assertEqual(response.status_code, 201)

transaction = Transaction.objects.get(transaction_id=self.payload["transaction_id"])
serializer = ProcessTransactionSerializer(transaction)
transaction_data = TransactionSerializer(transaction).data
transaction_data["item"] = TransactionItemSerializer(transaction.transaction_items.all()[0]).data

self.assertEqual(response.data, serializer.data)
for key, value in response.data["data"].items():
if key == "item":
for item_key, item_value in response.data["data"]["item"].items():
self.assertEqual(item_value, transaction_data["item"][item_key])
self.assertEqual(value, transaction_data[key])

def test_create_transaction_without_token(self):
"""
Test that a transaction cannot be created without a valid token.
"""
response = self.client.post(self.endpoint, self.payload, format="json")
self.assertEqual(response.status_code, 401)

def test_create_transaction_with_duplicate_transaction_id(self):
"""
Test that a transaction cannot be created with a duplicate transaction ID.
"""
# Create a new transaction with the same transaction ID as the payload
TransactionFactory.create(transaction_id=self.payload["transaction_id"])

self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key)

response = self.client.post(self.endpoint, self.payload, format="json")
response_data_message = json.loads(response.content)

self.assertEqual(response.status_code, 400)
self.assertEqual(
response_data_message["transaction_id"][0], "transaction with this transaction id already exists."
)

def test_create_transaction_with_invalid_fields(self):
"""
Test that a transaction cannot be created if fields are invalid.
"""
# Set the 'amount' field to an invalid value
self.payload["amount"] = "invalid_amount"

self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key)

response = self.client.post(self.endpoint, self.payload, format="json")
self.assertEqual(response.status_code, 400)

def test_create_transaction_with_missing_fields(self):
"""
Test that a transaction cannot be created if required fields are missing.
"""
# Remove the required 'transaction_id' field from the payload
del self.payload["transaction_id"]

self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key)

response = self.client.post(self.endpoint, self.payload, format="json")
self.assertEqual(response.status_code, 400)

def test_create_transaction_with_invalid_token(self):
"""
Test that a transaction cannot be created with an invalid token.
"""
self.client.credentials(HTTP_AUTHORIZATION="Token " + "invalid_token")

response = self.client.post(self.endpoint, self.payload, format="json")
self.assertEqual(response.status_code, 401)

def test_create_transaction_with_missing_item_field(self):
"""
Test that a transaction cannot be created if the 'item' field is missing.
"""
# Remove the required 'item' field from the payload
del self.payload["item"]

self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key)

response = self.client.post(self.endpoint, self.payload, format="json")
self.assertEqual(response.status_code, 400)
File renamed without changes.

0 comments on commit 423b215

Please sign in to comment.