From db29c6918cfd708c3fb1b21bbd99d247e157a411 Mon Sep 17 00:00:00 2001 From: Joonas Syysvirta Date: Wed, 12 Jun 2024 21:19:17 +0300 Subject: [PATCH 1/7] Install setuptools and wheel in CI pipeline Getting requirements to build wheel did not run succesfully due to and error in importing from setuptools. --- .github/workflows/respa-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/respa-ci.yml b/.github/workflows/respa-ci.yml index fad13cd82..a4d10dd2e 100644 --- a/.github/workflows/respa-ci.yml +++ b/.github/workflows/respa-ci.yml @@ -28,6 +28,7 @@ jobs: # the tests for that module which breaks. Also remove the location of source code. - name: Install python packages run: | + pip install setuptools wheel pip install -r requirements.txt # Not sure why some of the strings are not translated during test. Thus, From 511c8170c4eaf61f381504cc8fe928494c54863a Mon Sep 17 00:00:00 2001 From: Joonas Syysvirta Date: Wed, 12 Jun 2024 16:02:10 +0300 Subject: [PATCH 2/7] Add API endpoint for setting user email Add a REST API endpoint for setting the user's email address if it's empty. Refs STAM-5 --- users/api.py | 49 +++++++++++++++++++++++++- users/tests.py | 3 -- users/tests/__init__.py | 0 users/tests/test_api.py | 76 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 4 deletions(-) delete mode 100644 users/tests.py create mode 100644 users/tests/__init__.py create mode 100644 users/tests/test_api.py diff --git a/users/api.py b/users/api.py index 45cbfdc62..d97de6551 100644 --- a/users/api.py +++ b/users/api.py @@ -1,5 +1,8 @@ +from allauth.socialaccount.models import EmailAddress from django.contrib.auth import get_user_model -from rest_framework import permissions, serializers, generics, mixins, viewsets +from rest_framework import permissions, serializers, generics, viewsets, status +from rest_framework.decorators import action +from rest_framework.response import Response from resources.models.utils import build_ical_feed_url from resources.models import Unit @@ -69,6 +72,50 @@ def get_object(self): else: obj = self.request.user return obj + + @action(detail=True, methods=["post"], url_path="set_email") + def set_email(self, request, pk=None): + user = self.get_object() + email = request.data.get("email") + requesting_user = request.user + + if user != requesting_user: + return Response( + {"detail": "Not allowed to set email for this user."}, + status=status.HTTP_403_FORBIDDEN + ) + + if not email: + return Response( + {"detail": "Email address is required."}, + status=status.HTTP_400_BAD_REQUEST + ) + + if user.email: + return Response( + {"detail": "User already has an email set."}, + status=status.HTTP_400_BAD_REQUEST + ) + + if EmailAddress.objects.filter(email=email): + return Response( + {"detail": "The email address is already in use."}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Update the existing EmailAddress + email_address = EmailAddress.objects.filter(user=user) + if email_address: + email_address.update(email=email) + + # Update the User + user.email = email + user.save() + + return Response( + {"detail": "Email set successfully."}, + status=status.HTTP_200_OK + ) permission_classes = [permissions.IsAuthenticated] queryset = get_user_model().objects.all() diff --git a/users/tests.py b/users/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/users/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/users/tests/__init__.py b/users/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/users/tests/test_api.py b/users/tests/test_api.py new file mode 100644 index 000000000..8e8cffb84 --- /dev/null +++ b/users/tests/test_api.py @@ -0,0 +1,76 @@ +from allauth.account.models import EmailAddress +from django.contrib.auth import get_user_model +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APIClient, APITestCase + +User = get_user_model() + + +class SetUserEmailTests(APITestCase): + def setUp(self): + # Create a test user + self.user = User.objects.create_user( + username="testuser", password="testpassword", email="" + ) + self.client = APIClient() + self.client.force_authenticate(user=self.user) + EmailAddress.objects.create(user=self.user, email="") + + # Create another user + self.other_user = User.objects.create_user( + username="otheruser", password="otherpassword", email="other@example.com" + ) + EmailAddress.objects.create(user=self.other_user, email="other@example.com") + + def test_set_email_success(self): + url = reverse("user-set-email", kwargs={"pk": self.user.pk}) + data = {"email": "newemail@example.com"} + + response = self.client.post(url, data, format="json") + + self.user.refresh_from_db() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, {"detail": "Email set successfully."}) + self.assertEqual(self.user.email, "newemail@example.com") + self.assertTrue( + EmailAddress.objects.filter( + user=self.user, email="newemail@example.com" + ).exists() + ) + + def test_set_email_already_set(self): + # Set an email for the user first + self.user.email = "existingemail@example.com" + self.user.save() + + url = reverse("user-set-email", kwargs={"pk": self.user.pk}) + data = {"email": "anotheremail@example.com"} + + response = self.client.post(url, data, format="json") + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data, {"detail": "User already has an email set."}) + self.assertEqual(self.user.email, "existingemail@example.com") + + def test_set_email_not_provided(self): + url = reverse("user-set-email", kwargs={"pk": self.user.pk}) + data = {"email": ""} # No email in the data + + response = self.client.post(url, data, format="json") + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data, {"detail": "Email address is required."}) + self.assertEqual(self.user.email, "") + + def test_set_email_already_in_use(self): + url = reverse("user-set-email", kwargs={"pk": self.user.pk}) + data = {"email": "other@example.com"} # Email already used by other_user + + response = self.client.post(url, data, format="json") + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual( + response.data, {"detail": "The email address is already in use."} + ) + self.assertEqual(self.user.email, "") From 01df826ee92ebce536110fe57394ab8f2ffb4856 Mon Sep 17 00:00:00 2001 From: Joonas Syysvirta Date: Thu, 13 Jun 2024 10:40:08 +0300 Subject: [PATCH 3/7] Update django-tamusers Refs STAM-5 --- requirements.in | 2 +- requirements.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.in b/requirements.in index 20d0a7cac..f67552fa0 100644 --- a/requirements.in +++ b/requirements.in @@ -18,7 +18,7 @@ django-ckeditor==5.9.0 django-cors-headers django-image-cropping easy_thumbnails -git+https://github.com/Tampere/django-tamusers.git@9eb5f84171b1a2f525f28de408171608dd122574#egg=django-tamusers +git+https://github.com/Tampere/django-tamusers.git@31b9251bcd9e769bd446528009377834cb68e760#egg=django-tamusers django-allauth djangorestframework-jwt django-storages[google] diff --git a/requirements.txt b/requirements.txt index 28286b1a8..f697201d5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile +# pip-compile requirements.in # --no-binary psycopg2 @@ -45,7 +45,7 @@ django-parler==1.9.2 # via -r requirements.in, django-munigeo, django-parle django-reversion==3.0.2 # via -r requirements.in django-sanitized-dump==0.2.2 # via -r requirements.in django-storages[google]==1.12.3 # via -r requirements.in -git+https://github.com/Tampere/django-tamusers.git@9eb5f84171b1a2f525f28de408171608dd122574#egg=django-tamusers # via -r requirements.in +git+https://github.com/Tampere/django-tamusers.git@31b9251bcd9e769bd446528009377834cb68e760#egg=django-tamusers # via -r requirements.in django==2.2.11 # via -r requirements.in, django-admin-json-editor, django-allauth, django-anymail, django-filter, django-jinja, django-mptt, django-munigeo, django-reversion, django-storages, django-tamusers, drf-oidc-auth, easy-thumbnails djangorestframework-jwt==1.11.0 # via -r requirements.in djangorestframework==3.9.1 # via -r requirements.in, django-parler-rest, drf-oidc-auth From bc7a9acfe609229423d05a491a40f728557ed121 Mon Sep 17 00:00:00 2001 From: jorilindell Date: Mon, 19 Aug 2024 14:51:38 +0300 Subject: [PATCH 4/7] Upgrade docker to use postgres 14 --- docker/postgres/Dockerfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docker/postgres/Dockerfile b/docker/postgres/Dockerfile index 43969cc8f..4492ac3a4 100644 --- a/docker/postgres/Dockerfile +++ b/docker/postgres/Dockerfile @@ -1,7 +1,8 @@ -FROM postgres:10 +FROM postgres:14 RUN apt-get update && apt-get install --no-install-recommends -y \ - postgis postgresql-10-postgis-2.5 postgresql-10-postgis-2.5-scripts + postgis postgresql-14-postgis-3 postgresql-14-postgis-3-scripts \ + && apt-get clean RUN localedef -i fi_FI -c -f UTF-8 -A /usr/share/locale/locale.alias fi_FI.UTF-8 From 48d2eff41f79cd412c4dc363c97d001aa90d4887 Mon Sep 17 00:00:00 2001 From: jorilindell Date: Mon, 19 Aug 2024 14:54:18 +0300 Subject: [PATCH 5/7] Add 25.5% tax percentage to payments module --- .../migrations/0008_add_new_tax_percentage.py | 24 +++++++++++++++++++ payments/models.py | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 payments/migrations/0008_add_new_tax_percentage.py diff --git a/payments/migrations/0008_add_new_tax_percentage.py b/payments/migrations/0008_add_new_tax_percentage.py new file mode 100644 index 000000000..1deb4fc22 --- /dev/null +++ b/payments/migrations/0008_add_new_tax_percentage.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.11 on 2024-08-17 13:04 + +from decimal import Decimal +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('payments', '0007_add_invoice_model'), + ] + + operations = [ + migrations.AlterField( + model_name='orderline', + name='tax_percentage', + field=models.DecimalField(choices=[(Decimal('0.00'), '0.00'), (Decimal('10.00'), '10.00'), (Decimal('14.00'), '14.00'), (Decimal('24.00'), '24.00'), (Decimal('25.50'), '25.50')], decimal_places=2, default=Decimal('25.50'), max_digits=5, verbose_name='tax percentage'), + ), + migrations.AlterField( + model_name='sapmaterialcode', + name='tax_percentage', + field=models.DecimalField(choices=[(Decimal('0.00'), '0.00'), (Decimal('10.00'), '10.00'), (Decimal('14.00'), '14.00'), (Decimal('24.00'), '24.00'), (Decimal('25.50'), '25.50')], decimal_places=2, default=Decimal('25.50'), max_digits=5, verbose_name='tax percentage'), + ), + ] diff --git a/payments/models.py b/payments/models.py index db8f0e78d..a06bd536b 100644 --- a/payments/models.py +++ b/payments/models.py @@ -29,10 +29,11 @@ "10.00", "14.00", "24.00", + "25.50", ) ] -DEFAULT_TAX_PERCENTAGE = Decimal("24.00") +DEFAULT_TAX_PERCENTAGE = Decimal("25.50") PRICE_PER_PERIOD = "per_period" PRICE_FIXED = "fixed" PRICE_TYPE_CHOICES = ( From e2e51f3dda19e08e8c9513d07a2bed001d106bc9 Mon Sep 17 00:00:00 2001 From: jorilindell Date: Mon, 19 Aug 2024 14:54:34 +0300 Subject: [PATCH 6/7] Add 25.5% tax percentage to respa pricing module --- .../migrations/0008_add_new_tax_percentage.py | 24 +++++++++++++++++++ respa_pricing/models/price.py | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 respa_pricing/migrations/0008_add_new_tax_percentage.py diff --git a/respa_pricing/migrations/0008_add_new_tax_percentage.py b/respa_pricing/migrations/0008_add_new_tax_percentage.py new file mode 100644 index 000000000..06be99b19 --- /dev/null +++ b/respa_pricing/migrations/0008_add_new_tax_percentage.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.11 on 2024-08-17 15:15 + +from decimal import Decimal +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('respa_pricing', '0007_auto_20231218_1250'), + ] + + operations = [ + migrations.AlterField( + model_name='eventtype', + name='tax_percentage', + field=models.DecimalField(choices=[(Decimal('0.00'), '0.00'), (Decimal('10.00'), '10.00'), (Decimal('14.00'), '14.00'), (Decimal('24.00'), '24.00'), (Decimal('25.50'), '25.50')], decimal_places=2, default=Decimal('25.50'), max_digits=5, verbose_name='tax percentage'), + ), + migrations.AlterField( + model_name='usergroup', + name='tax_percentage', + field=models.DecimalField(choices=[(Decimal('0.00'), '0.00'), (Decimal('10.00'), '10.00'), (Decimal('14.00'), '14.00'), (Decimal('24.00'), '24.00'), (Decimal('25.50'), '25.50')], decimal_places=2, default=Decimal('25.50'), max_digits=5, verbose_name='tax percentage'), + ), + ] diff --git a/respa_pricing/models/price.py b/respa_pricing/models/price.py index 1d88c66ac..1414cc7b5 100644 --- a/respa_pricing/models/price.py +++ b/respa_pricing/models/price.py @@ -23,10 +23,11 @@ "10.00", "14.00", "24.00", + "25.50", ) ] -DEFAULT_TAX_PERCENTAGE = Decimal("24.00") +DEFAULT_TAX_PERCENTAGE = Decimal("25.50") PRICE_PER_PERIOD = "per_period" PRICE_FIXED = "fixed" From 7eddbcec42c460f4097dbad7501712031f6ff227 Mon Sep 17 00:00:00 2001 From: jorilindell Date: Mon, 19 Aug 2024 15:03:25 +0300 Subject: [PATCH 7/7] Add 25.5% tax percentage to Ceepos tax code mapping --- payments/providers/cpu_ceepos.py | 2 ++ payments/tests/test_cpu_ceepos.py | 45 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/payments/providers/cpu_ceepos.py b/payments/providers/cpu_ceepos.py index 84f26a054..1bb70ec9d 100644 --- a/payments/providers/cpu_ceepos.py +++ b/payments/providers/cpu_ceepos.py @@ -150,6 +150,7 @@ def _get_order_line_description(order: Order) -> str: def _get_ceepos_tax_code(order_line: OrderLine) -> str: tax_pct = order_line.tax_percentage ceepos_tax_codes = { + 25_500_000: "255", 24_000_000: "24", 14_000_000: "14", 10_000_000: "10", @@ -158,6 +159,7 @@ def _get_ceepos_tax_code(order_line: OrderLine) -> str: # Tampere specific Ceepos tax codes in the production environment if self.url_payment_api == "https://shop.tampere.fi/maksu.html": ceepos_tax_codes = { + 25_500_000: "35", 24_000_000: "15", 14_000_000: "14", 10_000_000: "13", diff --git a/payments/tests/test_cpu_ceepos.py b/payments/tests/test_cpu_ceepos.py index 09efb7887..832c11ac0 100644 --- a/payments/tests/test_cpu_ceepos.py +++ b/payments/tests/test_cpu_ceepos.py @@ -1,6 +1,7 @@ import hmac import json from unittest import mock +from decimal import Decimal import pytest from django.http import HttpResponse @@ -324,6 +325,50 @@ def test_payload_add_products_success(payment_provider, order_with_products): assert "Description" in product +@pytest.mark.parametrize( + "tax_percentage,tax_code", + ( + (Decimal('0'), "0"), + (Decimal('10.00'), "10"), + (Decimal('14.00'), "14"), + (Decimal('24.00'), "24"), + (Decimal('25.50'), "255"), + ), +) +def test_tax_code_mapping_in_qa(payment_provider, order_with_products, tax_percentage, tax_code): + """Test the tax percentage is mapped to a correct code in qa environment""" + payload = {} + + order_with_products.order_lines.all().update(tax_percentage=tax_percentage) + payment_provider.payload_add_products(payload, order_with_products) + + for product in payload["Products"]: + assert product["Taxcode"] == tax_code + + +@pytest.mark.parametrize( + "tax_percentage,tax_code", + ( + (Decimal('0'), "18"), + (Decimal('10.00'), "13"), + (Decimal('14.00'), "14"), + (Decimal('24.00'), "15"), + (Decimal('25.50'), "35"), + ), +) +def test_tax_code_mapping_in_production(provider_base_config, order_with_products, tax_percentage, tax_code): + """Test the tax percentage is mapped to a correct code in production environment""" + provider_base_config["RESPA_PAYMENTS_CEEPOS_API_URL"] = "https://shop.tampere.fi/maksu.html" + payment_provider = CPUCeeposProvider(config=provider_base_config) + payload = {} + + order_with_products.order_lines.all().update(tax_percentage=tax_percentage) + payment_provider.payload_add_products(payload, order_with_products) + + for product in payload["Products"]: + assert product["Taxcode"] == tax_code + + def test_payload_add_customer_success(payment_provider, order_with_products): """Test the customer data from order is added correctly into payload""" payload = {}