diff --git a/development_notebook/2024-03-21.md b/development_notebook/2024-03-21.md new file mode 100644 index 0000000..7df5efa --- /dev/null +++ b/development_notebook/2024-03-21.md @@ -0,0 +1,35 @@ +# State Of Work + +* Project has been fully configured from cookiecutter-django using MySQL. +* Deployed to production at [negligentoctopus.pythonanywhere.com](negligentoctopus.pythonanywhere.com). + +Core models are being defined and added to the django-admin. +Account and Transactions are implemented with balance as prefix-sum. + +## Update + +Testing has been defined. + +# Today + +Implement testings and fix bugs found. + +__To Continue__ +Define categories architecture for transactions + +## Work Log +__InProgress__ + +__ToDo__ + +__Done__ +* Add testing -- Functions are defined in class. Follow the one implemented as example. + * Test business logic + * Test balance is correct + * Test balance on account is safe for one, for many transaction, with or withput inital balance + * Test balance is same on change initial balance and change of a transaction + * Test that changing acc or timestamp fails + +__Discarded__ + +# To Do diff --git a/negligent_octopus/core/models.py b/negligent_octopus/core/models.py index b6d8ef8..efaacc6 100644 --- a/negligent_octopus/core/models.py +++ b/negligent_octopus/core/models.py @@ -1,7 +1,6 @@ from django.db import models from django.db import transaction as db_transaction from django.utils import timezone -from django.utils.functional import cached_property from model_utils.models import SoftDeletableModel from model_utils.models import TimeStampedModel @@ -13,11 +12,24 @@ class Account(TimeStampedModel, SoftDeletableModel): name = models.CharField(max_length=255) initial_balance = models.FloatField(default=0.0) - @cached_property + @property def balance(self): last_transaction = self.transaction_set.first() return last_transaction.balance if last_transaction else self.initial_balance + def save(self, *args, **kwargs): + try: + old_model = self.__class__.objects.get(pk=self.pk) + if ( + old_model.initial_balance != self.initial_balance + and hasattr(self, "transaction_set") + and self.transaction_set.last() is not None + ): + self.transaction_set.last().save() # Trigger update balance + except self.__class__.DoesNotExist: + pass + super().save(*args, **kwargs) + def __str__(self): return str(self.name) @@ -76,8 +88,8 @@ def save(self, *args, update_balance=True, **kwargs): return universe = self.account.transaction_set.filter( - models.Q(timestamp__gte=self.timestamp) - & models.Q(created__gt=self.created), + models.Q(timestamp__gt=self.timestamp) + | (models.Q(timestamp=self.timestamp) & models.Q(created__gt=self.created)), ) previous = self diff --git a/negligent_octopus/core/tests/test_models.py b/negligent_octopus/core/tests/test_models.py index c1114bc..a008ed4 100644 --- a/negligent_octopus/core/tests/test_models.py +++ b/negligent_octopus/core/tests/test_models.py @@ -1,3 +1,5 @@ +from datetime import timedelta + import pytest from django.utils import timezone from faker import Faker @@ -21,17 +23,121 @@ def test_account_balance(self, account: Account): ) assert account.balance == balance + def test_transaction_balance(self, account: Account): + balance = account.initial_balance + for i in range(10): + TransactionFactory( + account=account, + amount=i, + timestamp=timezone.now(), # Make sure they are in order + ) + for i, transaction in zip( + range(10), + account.transaction_set.all().reverse(), + strict=False, + ): + balance += i + assert transaction.balance == balance + def test_account_initial_balance_change(self, account: Account): - raise NotImplementedError + balance = account.initial_balance + for i in range(10): + TransactionFactory( + account=account, + amount=i, + timestamp=timezone.now(), # Make sure they are in order + ) + account.initial_balance += 1 + account.save() + + for i, transaction in zip( + range(10), + account.transaction_set.all().reverse(), + strict=False, + ): + balance += i + assert transaction.balance == balance + 1 - def test_transaction_added_last(self, account: Account): - raise NotImplementedError + def test_transaction_added_before(self, account: Account): + now = timezone.now() + TransactionFactory( + account=account, + amount=10, + timestamp=now, + ) + transaction = TransactionFactory( + account=account, + amount=10, + timestamp=now, + ) + old_balance = account.balance - def test_transaction_added_first(self, account: Account): - raise NotImplementedError + transaction_before = TransactionFactory( + account=account, + amount=11, + timestamp=now - timedelta(minutes=1), + ) + TransactionFactory( + account=account, + amount=11, + timestamp=now - timedelta(minutes=1), + ) + + assert account.transaction_set.first().pk == transaction.pk + assert account.transaction_set.last().pk == transaction_before.pk + + assert old_balance == account.initial_balance + 20 + assert account.balance == old_balance + 22 def test_transaction_added_middle(self, account: Account): - raise NotImplementedError + now = timezone.now() + first_transaction = TransactionFactory( + account=account, + amount=1, + timestamp=now, + ) + third_transaction = TransactionFactory( + account=account, + amount=100, + timestamp=now + timedelta(minutes=2), + ) + TransactionFactory( + account=account, + amount=10, + timestamp=now + timedelta(minutes=1), + ) + + # Reload instances + first_transaction = account.transaction_set.get(pk=first_transaction.pk) + third_transaction = account.transaction_set.get(pk=third_transaction.pk) + + assert first_transaction.balance == account.initial_balance + 1 + assert third_transaction.balance == account.initial_balance + 1 + 10 + 100 def test_transaction_change_amount(self, account: Account): - raise NotImplementedError + now = timezone.now() + first_transaction = TransactionFactory( + account=account, + amount=1, + timestamp=now, + ) + second_transaction = TransactionFactory( + account=account, + amount=10, + timestamp=now + timedelta(minutes=1), + ) + third_transaction = TransactionFactory( + account=account, + amount=100, + timestamp=now + timedelta(minutes=2), + ) + second_transaction.amount += 10 + second_transaction.save() + + # Reload instances + first_transaction = account.transaction_set.get(pk=first_transaction.pk) + third_transaction = account.transaction_set.get(pk=third_transaction.pk) + + assert first_transaction.balance == account.initial_balance + 1 + assert second_transaction.balance == account.initial_balance + 1 + 20 + assert third_transaction.balance == account.initial_balance + 1 + 20 + 100