diff --git a/users/tests/test_payments.py b/users/tests/test_payments.py new file mode 100644 index 00000000..29e0617c --- /dev/null +++ b/users/tests/test_payments.py @@ -0,0 +1,250 @@ +from django.contrib.auth import get_user_model +from django.test import TestCase + +from drfx import config +from datetime import date, timedelta +from utils import referencenumber +from utils.businesslogic import BusinessLogic + +from users.models import ( + BankTransaction, + CustomInvoice, + CustomUser, + MemberService, + ServiceSubscription, +) + + +class PaymentServices(TestCase): + """ + Tests for payment of services and custom invoices + """ + + parent_ref = referencenumber.generate(123) + child_ref = referencenumber.generate(456) + grandchild_ref = referencenumber.generate(789) + custom_invoice_ref = referencenumber.generate(101112) + + def setUp(self): + """ + Generate test user, test services and test + """ + self.user = get_user_model().objects.create_customuser( + first_name="FirstName", + last_name="LastName", + email="user1@example.com", + birthday=date.today(), + municipality="City", + nick="user1", + phone="+358123123", + ) + + self.grandchild_service = MemberService( + name="grandchild_service", + cost=10, + days_per_payment=70, + days_bonus_for_first=20, + ) + self.grandchild_service.save() + + self.child_service = MemberService( + name="child_service", + cost=10, + days_per_payment=60, + days_bonus_for_first=20, + pays_also_service=self.grandchild_service, + ) + self.child_service.save() + + self.parent_service = MemberService( + name="parent_service", + cost=30, + days_per_payment=30, + days_bonus_for_first=10, + pays_also_service=self.child_service, + ) + self.parent_service.save() + + self.parent_subscription = ServiceSubscription( + user=self.user, + service=self.parent_service, + state=ServiceSubscription.ACTIVE, + reference_number=self.parent_ref, + ) + self.parent_subscription.save() + + self.child_subscription = ServiceSubscription( + user=self.user, + service=self.child_service, + state=ServiceSubscription.ACTIVE, + reference_number=self.child_ref, + ) + self.child_subscription.save() + + self.grandchild_subscription = ServiceSubscription( + user=self.user, + service=self.grandchild_service, + state=ServiceSubscription.ACTIVE, + reference_number=self.grandchild_ref, + ) + self.grandchild_subscription.save() + + self.custom_invoice = CustomInvoice( + reference_number=self.custom_invoice_ref, + user=self.user, + days=100, + amount=120, + subscription=self.parent_subscription, + ) + self.custom_invoice.save() + + def test_static_parent_payment(self): + # + # First test parent payment pays all services + # + + amounts = [15, 30, 30, 30, 15] + # [not_enough_money, extradays, days_per_payment, days_per_payment, not_enough_money] + e_p_results = [0, 40, 70, 100, 100] + # virtual_payment = parent.daysleft - parent_days_per_payment + child_days_per_payment + # [not_enough_money, virtual_payment + extradays, virtual_payment, virtual_payment, not_enough_money] + e_c_results = [0, 90, 100, 130, 130] + # [not_enough_money, virtual_payment + extradays, virtual_payment 0: + for subscription in subscriptions: + results_row.append(subscription.days_left()) + + elif len(custominvoices) > 0: + for custominvoice in custominvoices: + results_row.append(custominvoice.subscription.days_left()) + else: + return False + + results.append(results_row) + transposed_results = [ + [results[j][i] for j in range(len(results))] for i in range(len(results[0])) + ] + if len(transposed_results) == 1: + result = [ + transposed_results[0][i] for i in range(len(transposed_results[0])) + ] + return result + else: + return transposed_results + + def tearDown(self): + CustomInvoice.objects.all().delete() + CustomUser.objects.all().delete() + MemberService.objects.all().delete() + ServiceSubscription.objects.all().delete() + BankTransaction.objects.all().delete() diff --git a/utils/businesslogic.py b/utils/businesslogic.py index 5eadd310..ae990e50 100644 --- a/utils/businesslogic.py +++ b/utils/businesslogic.py @@ -143,6 +143,7 @@ def updateuser(user): Updates the user's status based on the data in database. Can be called from outside. """ # Check for custom invoices.. + logger.debug("Examining custom invoices") invoices = CustomInvoice.objects.filter( user=user, payment_transaction__isnull=True ) @@ -297,39 +298,14 @@ def _check_transaction_pays_custominvoice(transaction): for invoice in invoices: if transaction.amount >= invoice.amount: - try: - logger.debug(f"Transaction {transaction} pays invoice {invoice}") - subscription = ServiceSubscription.objects.get( - user=invoice.user, id=invoice.subscription.id - ) - if not subscription.paid_until: - subscription.paid_until = transaction.date - subscription.paid_until = subscription.paid_until + timedelta( - days=invoice.days - ) - subscription.last_payment = transaction - invoice.payment_transaction = transaction - transaction.has_been_used = True - transaction.user = invoice.user - transaction.save() - invoice.save() - subscription.save() - transaction.user.log( - _( - "Paid %(days)s days of %(name)s, ending at %(until)s with transaction %(transaction)s" - % { - "days": invoice.days, - "name": subscription.service.name, - "until": subscription.paid_until, - "transaction": transaction, - } - ) - ) - BusinessLogic._check_servicesubscription_state(subscription) - except ServiceSubscription.DoesNotExist: - logger.debug( - "Transaction would pay for invoice but user has no servicesubscription??" - ) + subscription = ServiceSubscription.objects.get( + user=invoice.user, id=invoice.subscription.id + ) + BusinessLogic._service_paid_by_transaction( + subscription, transaction, invoice.days + ) + invoice.payment_transaction = transaction + invoice.save() else: transaction.comment = f"Insufficient amount for invoice {invoice}" transaction.save() @@ -377,7 +353,9 @@ def _updatesubscription(user, subscription, servicesubscriptions): logger.debug( f"Transaction is new and pays for service {subscription.service}" ) - BusinessLogic._service_paid_by_transaction(subscription, transaction) + BusinessLogic._service_paid_by_transaction( + subscription, transaction, subscription.service.days_per_payment + ) else: transaction.user = subscription.user transaction.comment = ( @@ -408,15 +386,16 @@ def _user_is_subscribed_to(servicesubscriptions, service): return False @staticmethod - def _service_paid_by_transaction(servicesubscription, transaction): + def _service_paid_by_transaction(servicesubscription, transaction, add_days): """ Called if transaction actually pays for extra time on given service subscription """ translation.activate(servicesubscription.user.language) - # How many days to add to subscription's paid until - days_to_add = timedelta(days=servicesubscription.service.days_per_payment) + logger.debug(f"Paying {servicesubscription} and gained {add_days} days more") + # How many days to add to subscription's paid until + days_to_add = timedelta(days=add_days) # First payment - initialize with payment date and add first time bonus days if not servicesubscription.paid_until: bonus_days = timedelta( @@ -425,7 +404,14 @@ def _service_paid_by_transaction(servicesubscription, transaction): logger.debug( f"{servicesubscription} paid for first time, adding bonus of {bonus_days}" ) - transaction.comment = f"First payment of {servicesubscription} - added {bonus_days.days} bonus days." + if transaction.comment: + transaction.comment = ( + transaction.comment + + f"\r\nFirst payment of {servicesubscription} - added {bonus_days.days} bonus days." + ) + else: + transaction.comment = f"First payment of {servicesubscription} - added {bonus_days.days} bonus days." + days_to_add = days_to_add + bonus_days servicesubscription.paid_until = transaction.date @@ -464,21 +450,50 @@ def _service_paid_by_transaction(servicesubscription, transaction): if paid_servicesubscription.state == ServiceSubscription.SUSPENDED: logger.debug("Service is suspended - no action") else: - extra_days = timedelta( - days=paid_servicesubscription.service.days_per_payment + # Calculate days between transaction and parent service paidto date + added_days = servicesubscription.paid_until - transaction.date + child_days = 0 + # check if and howmuch forehand child servce is paid, if not child_days=0 + if paid_servicesubscription.paid_until: + if paid_servicesubscription.paid_until > transaction.date: + child_date = ( + paid_servicesubscription.paid_until - transaction.date + ) + child_days = child_date.days + + # Calculate child subscription payment to happen at same time that latest parrent subsciption, + # useful with custominvoices that pays Parent subscription multiple times + # 1. calculate virtual payment day by substarctin one payment days from parentservice paid to date. + # 2. add days of on childservice payment days. + # 3. if child service is paid forehand substarct those days. + # Child services like yearly payment are practical to keep in sync + + extra_days = ( + added_days.days + - servicesubscription.service.days_per_payment + + paid_servicesubscription.service.days_per_payment + - child_days + ) + + logger.debug( + f"""Child process add days calculated by + {added_days.days} + - {servicesubscription.service.days_per_payment} + + {paid_servicesubscription.service.days_per_payment} + - {child_days} + = gained {extra_days} days more""" ) - paid_servicesubscription.paid_until = transaction.date + extra_days - paid_servicesubscription.last_payment = transaction - paid_servicesubscription.save() - servicesubscription.user.log( - _( - "%(servicename)s is now paid until %(until)s due to %(anotherservicename)s was paid" + + # But if child servie is paid forehand more than custominvoice would pay, paid to date is kept. + + if extra_days < 0: + logger.debug( + "Gained days are negative, using previous paid to date" ) - % { - "servicename": str(paid_servicesubscription), - "until": str(paid_servicesubscription.paid_until), - "anotherservicename": str(servicesubscription.service), - } + extra_days = 0 + + BusinessLogic._service_paid_by_transaction( + paid_servicesubscription, transaction, extra_days ) BusinessLogic._check_servicesubscription_state( diff --git a/utils/locale/fi/LC_MESSAGES/django.po b/utils/locale/fi/LC_MESSAGES/django.po index 75753f49..41b81364 100644 --- a/utils/locale/fi/LC_MESSAGES/django.po +++ b/utils/locale/fi/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-02-08 12:59+0000\n" +"POT-Creation-Date: 2025-02-11 16:56+0000\n" "PO-Revision-Date: 2020-10-21 02:15+0300\n" "Last-Translator: Sami Olmari \n" "Language-Team: \n" @@ -30,35 +30,18 @@ msgstr "Tilauksesi palveluun %(service_name)s on päättymässä" msgid "Bank transaction of %(amount)s€ dated %(date)s" msgstr "Tilisiirto %(amount)s€ päiväyksellä %(date)s" -#: utils/businesslogic.py:189 utils/businesslogic.py:205 +#: utils/businesslogic.py:190 utils/businesslogic.py:206 msgid "Accepted as member" msgstr "Hyväksytty jäseneksi" -#: utils/businesslogic.py:280 +#: utils/businesslogic.py:281 #, python-format msgid "Service %(servicename)s state changed from %(oldstate)s to %(newstate)s" msgstr "" "Jäsenpalvelun %(servicename)s tila muuttui %(oldstate)s:sta %(newstate)s:ksi" -#: utils/businesslogic.py:319 -#, python-format -msgid "" -"Paid %(days)s days of %(name)s, ending at %(until)s with transaction " -"%(transaction)s" -msgstr "" -"%(name)s on nyt maksettu %(days)s päivää lisää, %(until)s asti tilisiirrolla " -"%(transaction)s" - -#: utils/businesslogic.py:445 +#: utils/businesslogic.py:431 #, python-format msgid "%(servicename)s is now paid until %(until)s due to %(transaction)s" msgstr "" "%(servicename)s on nyt maksettu %(until)s asti tilisiirrolla %(transaction)s" - -#: utils/businesslogic.py:475 -#, python-format -msgid "" -"%(servicename)s is now paid until %(until)s due to %(anotherservicename)s " -"was paid" -msgstr "" -"%(servicename)s on nyt maksettu %(until)s asti koska %(anotherservicename)s" diff --git a/www/templates/www/user.html b/www/templates/www/user.html index 44fbb6aa..1c585519 100644 --- a/www/templates/www/user.html +++ b/www/templates/www/user.html @@ -139,7 +139,7 @@

{% trans 'Bank transactions' %}

{{ transaction.reference_number }} {{ transaction.sender|default_if_none:"" }} {{ transaction.message }} - {{ transaction.comment|default_if_none:"" }} + {{ transaction.comment|default_if_none:""|linebreaks}} {% endfor %}