From 219c4ad1d63d63eb34c8bbb5b87e9e02bc2a83fd Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Mon, 16 Oct 2023 22:46:13 +0200 Subject: [PATCH 01/28] refactored event serializer: update to free event only if there are no registrations --- app/content/serializers/event.py | 10 ++++- app/content/util/event_utils.py | 39 +++++++++++++++++ app/content/views/registration.py | 25 ++++++++--- .../0002_remove_order_expire_date.py | 17 ++++++++ app/payment/models/order.py | 3 +- app/payment/serializers/__init__.py | 2 +- app/payment/serializers/order.py | 42 ++++++++++--------- app/payment/tasks.py | 30 ++++++++++--- app/payment/urls.py | 2 +- app/payment/views/order.py | 32 +++++++++++++- app/tests/payment/test_order_integration.py | 28 +++++++------ .../payment/test_paid_event_integration.py | 34 ++++++++++++++- 12 files changed, 212 insertions(+), 52 deletions(-) create mode 100644 app/payment/migrations/0002_remove_order_expire_date.py diff --git a/app/content/serializers/event.py b/app/content/serializers/event.py index a31c89dca..e3ab74e17 100644 --- a/app/content/serializers/event.py +++ b/app/content/serializers/event.py @@ -178,8 +178,14 @@ def update(self, instance, validated_data): paytime=paid_information_data["paytime"], ) - if event.is_paid_event and not len(paid_information_data): - raise APIPaidEventCantBeChangedToFreeEventException() + if event.is_paid_event: + if not len(paid_information_data) and event.list_count > 0: + raise APIPaidEventCantBeChangedToFreeEventException() + + paid_event = PaidEvent.objects.get(event=event) + if paid_event: + paid_event.delete() + event.paid_information = None if len(paid_information_data): self.update_paid_information(event, paid_information_data) diff --git a/app/content/util/event_utils.py b/app/content/util/event_utils.py index 034b98cc4..7848bd1fc 100644 --- a/app/content/util/event_utils.py +++ b/app/content/util/event_utils.py @@ -11,6 +11,45 @@ ) +def start_payment_countdown(event, request, registration): + """ + Checks if event is a paid event + and starts the countdown for payment for an user. + """ + + if not event.is_paid_event: + return + + check_if_has_paid.apply_async( + args=(event, registration), countdown=get_countdown_time(event) + ) + + +def get_countdown_time(event): + paytime = event.paid_information.paytime + return (paytime.hour * 60 + paytime.minute) * 60 + paytime.second + + +def create_vipps_order(order_id, event): + """ + Creates vipps order, and returns the url. + """ + + access_token = os.environ.get("PAYMENT_ACCESS_TOKEN") + expires_at = os.environ.get("PAYMENT_ACCESS_TOKEN_EXPIRES_AT") + + if not access_token or datetime.now() >= datetime.fromtimestamp(int(expires_at)): + (expires_at, access_token) = get_new_access_token() + os.environ.update({"PAYMENT_ACCESS_TOKEN": access_token}) + os.environ.update({"PAYMENT_ACCESS_TOKEN_EXPIRES_AT": str(expires_at)}) + + event_price = int(event.paid_information.price * 100) + + response = initiate_payment(event_price, str(order_id), event.title, access_token) + + return response["url"] + + def create_payment_order(event, request, registration): """ Checks if event is a paid event diff --git a/app/content/views/registration.py b/app/content/views/registration.py index 4dfd3437b..3fe0f3d12 100644 --- a/app/content/views/registration.py +++ b/app/content/views/registration.py @@ -13,7 +13,10 @@ from app.content.mixins import APIRegistrationErrorsMixin from app.content.models import Event, Registration from app.content.serializers import RegistrationSerializer -from app.content.util.event_utils import create_payment_order +from app.content.util.event_utils import ( + create_payment_order, + start_payment_countdown, +) from app.payment.models.order import Order from app.payment.views.vipps_callback import vipps_callback @@ -68,17 +71,29 @@ def create(self, request, *args, **kwargs): ) try: - create_payment_order(event, request, registration) - except Exception as order_error: - capture_exception(order_error) + start_payment_countdown(event, request, registration) + except Exception as countdown_error: + capture_exception(countdown_error) registration.delete() return Response( { - "detail": "Det skjedde en feil med opprettelse av betalingsordre. Påmeldingen ble ikke fullført." + "detail": "Det skjedde en feil med oppstart av betalingsfrist. Påmeldingen ble ikke fullført." }, status=status.HTTP_400_BAD_REQUEST, ) + # try: + # create_payment_order(event, request, registration) + # except Exception as order_error: + # capture_exception(order_error) + # registration.delete() + # return Response( + # { + # "detail": "Det skjedde en feil med opprettelse av betalingsordre. Påmeldingen ble ikke fullført." + # }, + # status=status.HTTP_400_BAD_REQUEST, + # ) + registration_serializer = RegistrationSerializer( registration, context={"user": registration.user} ) diff --git a/app/payment/migrations/0002_remove_order_expire_date.py b/app/payment/migrations/0002_remove_order_expire_date.py new file mode 100644 index 000000000..7dbabb71a --- /dev/null +++ b/app/payment/migrations/0002_remove_order_expire_date.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.5 on 2023-10-16 20:20 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("payment", "0001_initial"), + ] + + operations = [ + migrations.RemoveField( + model_name="order", + name="expire_date", + ), + ] diff --git a/app/payment/models/order.py b/app/payment/models/order.py index 31e9381d2..016ca5d89 100644 --- a/app/payment/models/order.py +++ b/app/payment/models/order.py @@ -25,7 +25,6 @@ class Order(BaseModel, BasePermissionModel): status = models.CharField( choices=OrderStatus.choices, default=OrderStatus.INITIATE, max_length=16 ) - expire_date = models.DateTimeField() payment_link = models.URLField(max_length=2000) class Meta: @@ -33,7 +32,7 @@ class Meta: ordering = ("-created_at",) def __str__(self): - return f"{self.order_id} {self.user} {self.event} {self.status} {self.expire_date}" + return f"{self.user} - {self.event} - {self.status} - {self.created_at}" @property def expired(self): diff --git a/app/payment/serializers/__init__.py b/app/payment/serializers/__init__.py index 8789a8df3..028a0b965 100644 --- a/app/payment/serializers/__init__.py +++ b/app/payment/serializers/__init__.py @@ -1 +1 @@ -from .order import OrderSerializer +from app.payment.serializers.order import OrderSerializer, OrderCreateSerializer diff --git a/app/payment/serializers/order.py b/app/payment/serializers/order.py index 51fd13a05..264e88988 100644 --- a/app/payment/serializers/order.py +++ b/app/payment/serializers/order.py @@ -1,31 +1,33 @@ +import uuid + from app.common.serializers import BaseModelSerializer -from app.content.models.event import Event -from app.content.models.user import User -from app.content.serializers.user import DefaultUserSerializer +from app.content.util.event_utils import create_vipps_order from app.payment.models.order import Order -from app.util.utils import now class OrderSerializer(BaseModelSerializer): class Meta: model = Order - fields = ("order_id", "status", "expire_date", "payment_link", "event", "user") - + fields = ("order_id", "status", "payment_link") -class OrderUpdateCreateSerializer(BaseModelSerializer): - user = DefaultUserSerializer(read_only=True) +class OrderCreateSerializer(BaseModelSerializer): class Meta: model = Order - fields = ("order_id", "user", "status", "expire_date") - - read_only_fields = "user" - - def create(self, validated_data): - user = User.objects.get(user_id=self.context["user_id"]) - paytime = Event.objects.get( - id=validated_data.get("event") - ).paid_information.paytime - return Order.objects.create( - user=user, expired_date=now() + paytime, **validated_data - ) + fields = ("event",) + + def create(self, validated_data): + user = validated_data.pop("user") + event = validated_data.pop("event") + + order_id = uuid.uuid4() + payment_url = create_vipps_order(order_id, event) + + order = Order.objects.create( + order_id=order_id, + payment_link=payment_url, + event=event, + user=user, + ) + + return order diff --git a/app/payment/tasks.py b/app/payment/tasks.py index 671ebf701..750f28267 100644 --- a/app/payment/tasks.py +++ b/app/payment/tasks.py @@ -7,19 +7,39 @@ from app.payment.views.vipps_callback import vipps_callback from app.util.tasks import BaseTask +# @app.task(bind=True, base=BaseTask) +# def check_if_has_paid(self, order_id, registration_id): +# try: +# vipps_callback(None, order_id) +# order = Order.objects.get(order_id=order_id) +# order_status = order.status +# if ( +# order_status != OrderStatus.CAPTURE +# and order_status != OrderStatus.RESERVE +# and order_status != OrderStatus.SALE +# ): +# Registration.objects.filter(registration_id=registration_id).delete() + +# except Order.DoesNotExist as order_not_exist: +# capture_exception(order_not_exist) + @app.task(bind=True, base=BaseTask) -def check_if_has_paid(self, order_id, registration_id): +def check_if_has_paid(self, event, registration): try: - vipps_callback(None, order_id) - order = Order.objects.get(order_id=order_id) - order_status = order.status + user_order = Order.objects.get(event=event, user=registration.user) + + vipps_callback(None, user_order.order_id) + order_status = user_order.status + if ( order_status != OrderStatus.CAPTURE and order_status != OrderStatus.RESERVE and order_status != OrderStatus.SALE ): - Registration.objects.filter(registration_id=registration_id).delete() + Registration.objects.filter( + registration_id=registration.registration_id + ).delete() except Order.DoesNotExist as order_not_exist: capture_exception(order_not_exist) diff --git a/app/payment/urls.py b/app/payment/urls.py index c7e2854a6..6ba1aa483 100644 --- a/app/payment/urls.py +++ b/app/payment/urls.py @@ -6,7 +6,7 @@ router = routers.DefaultRouter() -router.register("payment", OrderViewSet, basename="payment") +router.register("payments", OrderViewSet, basename="payment") urlpatterns = [ re_path(r"", include(router.urls)), diff --git a/app/payment/views/order.py b/app/payment/views/order.py index 49ddb2859..da82b931b 100644 --- a/app/payment/views/order.py +++ b/app/payment/views/order.py @@ -1,15 +1,19 @@ +from django.shortcuts import get_object_or_404 from rest_framework import status from rest_framework.response import Response from sentry_sdk import capture_exception from app.common.mixins import ActionMixin +from app.common.permissions import BasicViewPermission from app.common.viewsets import BaseViewSet +from app.content.models import User from app.payment.models import Order -from app.payment.serializers import OrderSerializer +from app.payment.serializers import OrderCreateSerializer, OrderSerializer class OrderViewSet(BaseViewSet, ActionMixin): + permission_classes = [BasicViewPermission] serializer_class = OrderSerializer queryset = Order.objects.all() @@ -28,3 +32,29 @@ def retrieve(self, request, pk): {"detail": "Fant ikke beatlingsordre."}, status=status.HTTP_404_NOT_FOUND, ) + + def create(self, request, *args, **kwargs): + try: + user = get_object_or_404(User, user_id=request.id) + + serializer = OrderCreateSerializer( + data=request.data, + context={"request": request}, + ) + + if serializer.is_valid(): + order = super().perform_create(serializer, user=user) + serializer = OrderSerializer( + order, context={"request": request}, many=False + ) + + return Response(serializer.data, status.HTTP_201_CREATED) + + return Response(serializer.errors, status.HTTP_400_BAD_REQUEST) + + except User.DoesNotExist as user_not_exist: + capture_exception(user_not_exist) + return Response( + {"detail": "Fant ikke bruker."}, + status=status.HTTP_404_NOT_FOUND, + ) diff --git a/app/tests/payment/test_order_integration.py b/app/tests/payment/test_order_integration.py index 55d2413fc..d4ae45663 100644 --- a/app/tests/payment/test_order_integration.py +++ b/app/tests/payment/test_order_integration.py @@ -1,21 +1,23 @@ -API_EVENT_BASE_URL = "/events/" -API_PAYMENT_BASE_URL = "/payment/" +import pytest +from app.payment.enums import OrderStatus +from app.util.test_utils import get_api_client -def _get_registration_url(event): - return f"{API_EVENT_BASE_URL}{event.pk}/registrations/" +API_PAYMENT_BASE_URL = "/payments/" -def _get_order_url(): - return f"{API_PAYMENT_BASE_URL}order/" +def get_order_data(event): + return {"event": event.id} -def _get_registration_post_data(user, event): - return { - "user_id": user.user_id, - "event": event.pk, - } +@pytest.mark.django_db +def test_create_paid_event_order(user, paid_event): + client = get_api_client(user=user) + data = get_order_data(paid_event.event) + response = client.post(f"{API_PAYMENT_BASE_URL}", data=data) -def _get_order_data(user, event): - return {"user_id": user.user_id, "event": event.pk} + order = response.data + + assert response.status_code == 201 + assert order["status"] == OrderStatus.INITIATE diff --git a/app/tests/payment/test_paid_event_integration.py b/app/tests/payment/test_paid_event_integration.py index a759cdfa5..b6f629688 100644 --- a/app/tests/payment/test_paid_event_integration.py +++ b/app/tests/payment/test_paid_event_integration.py @@ -136,9 +136,11 @@ def test_update_paid_event_as_admin(admin_user): @pytest.mark.django_db -def test_update_paid_event_to_free_event_as_admin(admin_user): +def test_update_paid_event_to_free_event_with_registrations_as_admin( + admin_user, registration +): """ - HS and Index members should not be able to update a paid event to a free event. + HS and Index members should not be able to update a paid event with registrations to a free event. Other subgroup members can update paid events where event.organizer is their group or None. Leaders of committees and interest groups should be able to update events where event.organizer is their group or None. @@ -146,6 +148,10 @@ def test_update_paid_event_to_free_event_as_admin(admin_user): paid_event = PaidEventFactory(price=100.00) event = paid_event.event + + registration.event = event + registration.save() + organizer = Group.objects.get_or_create(name="HS", type=GroupType.BOARD)[0] client = get_api_client(user=admin_user) url = get_events_url_detail(event) @@ -156,3 +162,27 @@ def test_update_paid_event_to_free_event_as_admin(admin_user): assert response.status_code == 400 assert event.is_paid_event + + +@pytest.mark.django_db +def test_update_paid_event_to_free_event_without_registrations_as_admin(admin_user): + """ + HS and Index members should be able to update a paid event without registrations to a free event. + Other subgroup members can update paid events where event.organizer is their group or None. + Leaders of committees and interest groups should be able to + update events where event.organizer is their group or None. + """ + + paid_event = PaidEventFactory(price=100.00) + event = paid_event.event + + organizer = Group.objects.get_or_create(name="HS", type=GroupType.BOARD)[0] + client = get_api_client(user=admin_user) + url = get_events_url_detail(event) + data = get_paid_event_data(organizer=organizer.slug, is_paid_event=False) + + response = client.put(url, data) + event.refresh_from_db() + + assert response.status_code == 200 + assert not event.is_paid_event From bbe23256b06eb38f1d218bf0b095324e429e8d9d Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Tue, 17 Oct 2023 20:18:38 +0200 Subject: [PATCH 02/28] first version finished --- app/communication/enums.py | 1 + .../0054_registration_payment_expiredate.py | 18 +++ app/content/models/registration.py | 44 ++++- app/content/serializers/event.py | 13 +- app/content/serializers/registration.py | 31 ++-- app/content/util/event_utils.py | 151 +++++++++++------- app/content/util/registration_utils.py | 9 ++ app/content/views/registration.py | 20 +-- app/payment/models/order.py | 4 +- app/payment/serializers/order.py | 6 +- app/payment/tasks.py | 7 +- app/payment/tests/test_order_model.py | 27 ---- app/payment/util/order_utils.py | 12 ++ app/payment/util/payment_utils.py | 39 ++++- 14 files changed, 249 insertions(+), 133 deletions(-) create mode 100644 app/content/migrations/0054_registration_payment_expiredate.py create mode 100644 app/content/util/registration_utils.py delete mode 100644 app/payment/tests/test_order_model.py create mode 100644 app/payment/util/order_utils.py diff --git a/app/communication/enums.py b/app/communication/enums.py index 43789dd90..60cb68ac1 100644 --- a/app/communication/enums.py +++ b/app/communication/enums.py @@ -3,6 +3,7 @@ class UserNotificationSettingType(models.TextChoices): REGISTRATION = "REGISTRATION", "Påmeldingsoppdateringer" + UNREGISTRATION = "UNREGISTRATION", "Avmeldingsoppdateringer" STRIKE = "STRIKE", "Prikkoppdateringer" EVENT_SIGN_UP_START = "EVENT_SIGN_UP_START", "Arrangementer - påmeldingsstart" EVENT_SIGN_OFF_DEADLINE = ( diff --git a/app/content/migrations/0054_registration_payment_expiredate.py b/app/content/migrations/0054_registration_payment_expiredate.py new file mode 100644 index 000000000..595bc57ee --- /dev/null +++ b/app/content/migrations/0054_registration_payment_expiredate.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.5 on 2023-10-17 15:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("content", "0053_event_contact_person"), + ] + + operations = [ + migrations.AddField( + model_name="registration", + name="payment_expiredate", + field=models.DateTimeField(default=None, null=True), + ), + ] diff --git a/app/content/models/registration.py b/app/content/models/registration.py index 0f3a249fb..7418fccf1 100644 --- a/app/content/models/registration.py +++ b/app/content/models/registration.py @@ -1,4 +1,4 @@ -from datetime import timedelta +from datetime import datetime, timedelta from django.core.exceptions import ValidationError from django.db import models @@ -17,7 +17,9 @@ from app.content.models.event import Event from app.content.models.strike import create_strike from app.content.models.user import User +from app.content.util.registration_utils import get_payment_expiredate from app.forms.enums import EventFormType +from app.payment.util.order_utils import check_if_order_is_paid from app.util import now from app.util.models import BaseModel from app.util.utils import datetime_format @@ -36,6 +38,7 @@ class Registration(BaseModel, BasePermissionModel): is_on_wait = models.BooleanField(default=False, verbose_name="waiting list") has_attended = models.BooleanField(default=False) allow_photo = models.BooleanField(default=True) + payment_expiredate = models.DateTimeField(null=True, default=None) class Meta: ordering = ("event", "created_at", "is_on_wait") @@ -85,6 +88,23 @@ def delete_submission_if_exists(self): )[:1] Submission.objects.filter(form=event_form, user=self.user).delete() + def refund_payment_if_exist(self): + from app.content.util.event_utils import refund_vipps_order + + if not self.event.is_paid_event: + return + + order = self.event.orders.filter(user=self.user).first() + + if check_if_order_is_paid(order): + refund_vipps_order( + order_id=order.order_id, + event=self.event, + transaction_text=f"Refund for {self.event.title} - {self.user.first_name} {self.user.last_name}", + ) + + self.send_notification_and_mail_for_refund(order) + def delete(self, *args, **kwargs): moved_registration = None if not self.is_on_wait: @@ -98,6 +118,9 @@ def delete(self, *args, **kwargs): moved_registration = self.move_from_waiting_list_to_queue() self.delete_submission_if_exists() + + self.refund_payment_if_exist() + registration = super().delete(*args, **kwargs) if moved_registration: moved_registration.save() @@ -184,6 +207,19 @@ def send_notification_and_mail(self): self.event.pk ).send() + def send_notification_and_mail_for_refund(self, order): + Notify( + [self.user], + f'Du har blitt meldt av "{self.event.title}" og vil bli refundert', + UserNotificationSettingType.UNREGISTRATION, + ).add_paragraph(f"Hei, {self.user.first_name}!").add_paragraph( + f"Du har blitt meldt av {self.event.title} og vil bli refundert." + ).add_paragraph( + f"Du vil få pengene tilbake på kontoen din innen kort tid." + ).add_paragraph( + f"Hvis det skulle oppstå noen problemer så kontakt oss på hs@tihlde.org. Ditt ordrenummer er {order.order_id}." + ).send() + def should_swap_with_non_prioritized_user(self): return ( self.is_on_wait @@ -235,6 +271,12 @@ def move_from_waiting_list_to_queue(self): registrations_in_waiting_list[0], ) registration_move_to_queue.is_on_wait = False + + if self.event.is_paid_event: + registration_move_to_queue.payment_expiredate = get_payment_expiredate( + self.event + ) + return registration_move_to_queue def clean(self): diff --git a/app/content/serializers/event.py b/app/content/serializers/event.py index e3ab74e17..28b98248e 100644 --- a/app/content/serializers/event.py +++ b/app/content/serializers/event.py @@ -171,12 +171,6 @@ def update(self, instance, validated_data): priority_pools_data = validated_data.pop("priority_pools", None) paid_information_data = validated_data.pop("paid_information", None) event = super().update(instance, validated_data) - if paid_information_data and not event.is_paid_event: - PaidEvent.objects.create( - event=event, - price=paid_information_data["price"], - paytime=paid_information_data["paytime"], - ) if event.is_paid_event: if not len(paid_information_data) and event.list_count > 0: @@ -187,6 +181,13 @@ def update(self, instance, validated_data): paid_event.delete() event.paid_information = None + if paid_information_data and not event.is_paid_event: + PaidEvent.objects.create( + event=event, + price=paid_information_data["price"], + paytime=paid_information_data["paytime"], + ) + if len(paid_information_data): self.update_paid_information(event, paid_information_data) diff --git a/app/content/serializers/registration.py b/app/content/serializers/registration.py index 0ca742d0a..4f46806ab 100644 --- a/app/content/serializers/registration.py +++ b/app/content/serializers/registration.py @@ -1,3 +1,5 @@ +from datetime import datetime, timedelta + from rest_framework import serializers from app.common.serializers import BaseModelSerializer @@ -6,17 +8,16 @@ DefaultUserSerializer, UserListSerializer, ) +from app.content.util.registration_utils import get_payment_expiredate from app.forms.enums import EventFormType from app.forms.serializers.submission import SubmissionInRegistrationSerializer -from app.payment.enums import OrderStatus -from app.payment.serializers.order import OrderSerializer +from app.payment.util.order_utils import check_if_order_is_paid class RegistrationSerializer(BaseModelSerializer): user_info = UserListSerializer(source="user", read_only=True) survey_submission = serializers.SerializerMethodField() has_unanswered_evaluation = serializers.SerializerMethodField() - order = serializers.SerializerMethodField(required=False) has_paid_order = serializers.SerializerMethodField(required=False) class Meta: @@ -30,7 +31,7 @@ class Meta: "created_at", "survey_submission", "has_unanswered_evaluation", - "order", + "payment_expiredate", "has_paid_order", ) @@ -41,20 +42,18 @@ def get_survey_submission(self, obj): def get_has_unanswered_evaluation(self, obj): return obj.user.has_unanswered_evaluations_for(obj.event) - def get_order(self, obj): + def get_has_paid_order(self, obj): order = obj.event.orders.filter(user=obj.user).first() - if order: - return OrderSerializer(order).data - return None - def get_has_paid_order(self, obj): - for order in obj.event.orders.filter(user=obj.user): - if ( - order.status == OrderStatus.CAPTURE - or order.status == OrderStatus.RESERVE - or order.status == OrderStatus.SALE - ): - return True + return check_if_order_is_paid(order) + + def create(self, validated_data): + event = validated_data["event"] + + if event.is_paid_event and not event.is_full: + validated_data["payment_expiredate"] = get_payment_expiredate(event) + + return super().create(validated_data) class PublicRegistrationSerializer(BaseModelSerializer): diff --git a/app/content/util/event_utils.py b/app/content/util/event_utils.py index 7848bd1fc..138f6efb7 100644 --- a/app/content/util/event_utils.py +++ b/app/content/util/event_utils.py @@ -1,28 +1,31 @@ import os import uuid -from datetime import datetime, timedelta +from datetime import datetime -from app.payment.enums import OrderStatus -from app.payment.models.order import Order from app.payment.tasks import check_if_has_paid from app.payment.util.payment_utils import ( get_new_access_token, initiate_payment, + refund_payment, ) -def start_payment_countdown(event, request, registration): +def start_payment_countdown(event, registration): """ Checks if event is a paid event and starts the countdown for payment for an user. """ - if not event.is_paid_event: + if not event.is_paid_event or registration.is_on_wait: return - check_if_has_paid.apply_async( - args=(event, registration), countdown=get_countdown_time(event) - ) + try: + check_if_has_paid.apply_async( + args=(event.id, registration.registration_id), + countdown=get_countdown_time(event), + ) + except Exception as e: + print(e) def get_countdown_time(event): @@ -30,7 +33,7 @@ def get_countdown_time(event): return (paytime.hour * 60 + paytime.minute) * 60 + paytime.second -def create_vipps_order(order_id, event): +def create_vipps_order(order_id, event, transaction_text): """ Creates vipps order, and returns the url. """ @@ -45,61 +48,89 @@ def create_vipps_order(order_id, event): event_price = int(event.paid_information.price * 100) - response = initiate_payment(event_price, str(order_id), event.title, access_token) + response = initiate_payment( + amount=event_price, + order_id=str(order_id), + access_token=access_token, + transaction_text=transaction_text, + ) return response["url"] -def create_payment_order(event, request, registration): +def refund_vipps_order(order_id, event, transaction_text): """ - Checks if event is a paid event - and creates a new Vipps payment order. + Refunds vipps order. """ - if event.is_paid_event: - access_token = os.environ.get("PAYMENT_ACCESS_TOKEN") - expires_at = os.environ.get("PAYMENT_ACCESS_TOKEN_EXPIRES_AT") - if not access_token or datetime.now() >= datetime.fromtimestamp( - int(expires_at) - ): - (expires_at, access_token) = get_new_access_token() - os.environ.update({"PAYMENT_ACCESS_TOKEN": access_token}) - os.environ.update({"PAYMENT_ACCESS_TOKEN_EXPIRES_AT": str(expires_at)}) - - prev_orders = Order.objects.filter(event=event, user=request.user) - has_paid_order = False - - for order in prev_orders: - if ( - order.status == OrderStatus.CAPTURE - or order.status == OrderStatus.RESERVE - or order.status == OrderStatus.SALE - ): - has_paid_order = True - break - - if not has_paid_order: - - paytime = event.paid_information.paytime - - expire_date = datetime.now() + timedelta( - hours=paytime.hour, minutes=paytime.minute, seconds=paytime.second - ) - - # Create Order - order_id = uuid.uuid4() - amount = int(event.paid_information.price * 100) - res = initiate_payment(amount, str(order_id), event.title, access_token) - payment_link = res["url"] - order = Order.objects.create( - order_id=order_id, - user=request.user, - event=event, - payment_link=payment_link, - expire_date=expire_date, - ) - order.save() - check_if_has_paid.apply_async( - args=(order.order_id, registration.registration_id), - countdown=(paytime.hour * 60 + paytime.minute) * 60 + paytime.second, - ) + access_token = os.environ.get("PAYMENT_ACCESS_TOKEN") + expires_at = os.environ.get("PAYMENT_ACCESS_TOKEN_EXPIRES_AT") + + if not access_token or datetime.now() >= datetime.fromtimestamp(int(expires_at)): + (expires_at, access_token) = get_new_access_token() + os.environ.update({"PAYMENT_ACCESS_TOKEN": access_token}) + os.environ.update({"PAYMENT_ACCESS_TOKEN_EXPIRES_AT": str(expires_at)}) + + event_price = int(event.paid_information.price * 100) + + refund_payment( + amount=event_price, + order_id=str(order_id), + access_token=access_token, + transaction_text=transaction_text, + ) + + +# def create_payment_order(event, request, registration): +# """ +# Checks if event is a paid event +# and creates a new Vipps payment order. +# """ +# print("run") +# if event.is_paid_event: +# access_token = os.environ.get("PAYMENT_ACCESS_TOKEN") +# expires_at = os.environ.get("PAYMENT_ACCESS_TOKEN_EXPIRES_AT") +# if not access_token or datetime.now() >= datetime.fromtimestamp( +# int(expires_at) +# ): +# (expires_at, access_token) = get_new_access_token() +# os.environ.update({"PAYMENT_ACCESS_TOKEN": access_token}) +# os.environ.update({"PAYMENT_ACCESS_TOKEN_EXPIRES_AT": str(expires_at)}) + +# prev_orders = Order.objects.filter(event=event, user=request.user) +# has_paid_order = False + +# print(prev_orders) +# for order in prev_orders: +# if ( +# order.status == OrderStatus.CAPTURE +# or order.status == OrderStatus.RESERVE +# or order.status == OrderStatus.SALE +# ): +# has_paid_order = True +# break + +# if not has_paid_order: +# print("has not paid order") +# paytime = event.paid_information.paytime + +# expire_date = datetime.now() + timedelta( +# hours=paytime.hour, minutes=paytime.minute, seconds=paytime.second +# ) + +# # Create Order +# order_id = uuid.uuid4() +# amount = int(event.paid_information.price * 100) +# res = initiate_payment(amount, str(order_id), event.title, access_token) +# payment_link = res["url"] +# order = Order.objects.create( +# order_id=order_id, +# user=request.user, +# event=event, +# payment_link=payment_link +# ) +# order.save() +# check_if_has_paid.apply_async( +# args=(order.order_id, registration.registration_id), +# countdown=(paytime.hour * 60 + paytime.minute) * 60 + paytime.second, +# ) diff --git a/app/content/util/registration_utils.py b/app/content/util/registration_utils.py new file mode 100644 index 000000000..1e024c7b7 --- /dev/null +++ b/app/content/util/registration_utils.py @@ -0,0 +1,9 @@ +from datetime import datetime, timedelta + + +def get_payment_expiredate(event): + return datetime.now() + timedelta( + hours=event.paid_information.paytime.hour, + minutes=event.paid_information.paytime.minute, + seconds=event.paid_information.paytime.second, + ) diff --git a/app/content/views/registration.py b/app/content/views/registration.py index 3fe0f3d12..7c04b5389 100644 --- a/app/content/views/registration.py +++ b/app/content/views/registration.py @@ -13,10 +13,7 @@ from app.content.mixins import APIRegistrationErrorsMixin from app.content.models import Event, Registration from app.content.serializers import RegistrationSerializer -from app.content.util.event_utils import ( - create_payment_order, - start_payment_countdown, -) +from app.content.util.event_utils import start_payment_countdown from app.payment.models.order import Order from app.payment.views.vipps_callback import vipps_callback @@ -61,6 +58,7 @@ def create(self, request, *args, **kwargs): request.data["allow_photo"] = request.user.allows_photo_by_default serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) event_id = self.kwargs.get("event_id", None) @@ -71,7 +69,7 @@ def create(self, request, *args, **kwargs): ) try: - start_payment_countdown(event, request, registration) + start_payment_countdown(event, registration) except Exception as countdown_error: capture_exception(countdown_error) registration.delete() @@ -82,18 +80,6 @@ def create(self, request, *args, **kwargs): status=status.HTTP_400_BAD_REQUEST, ) - # try: - # create_payment_order(event, request, registration) - # except Exception as order_error: - # capture_exception(order_error) - # registration.delete() - # return Response( - # { - # "detail": "Det skjedde en feil med opprettelse av betalingsordre. Påmeldingen ble ikke fullført." - # }, - # status=status.HTTP_400_BAD_REQUEST, - # ) - registration_serializer = RegistrationSerializer( registration, context={"user": registration.user} ) diff --git a/app/payment/models/order.py b/app/payment/models/order.py index 016ca5d89..869fb65b8 100644 --- a/app/payment/models/order.py +++ b/app/payment/models/order.py @@ -31,8 +31,8 @@ class Meta: verbose_name_plural = "Orders" ordering = ("-created_at",) - def __str__(self): - return f"{self.user} - {self.event} - {self.status} - {self.created_at}" + def __str__(self): + return f"{self.user} - {self.event.title} - {self.status} - {self.created_at}" @property def expired(self): diff --git a/app/payment/serializers/order.py b/app/payment/serializers/order.py index 264e88988..adf7d4576 100644 --- a/app/payment/serializers/order.py +++ b/app/payment/serializers/order.py @@ -21,7 +21,11 @@ def create(self, validated_data): event = validated_data.pop("event") order_id = uuid.uuid4() - payment_url = create_vipps_order(order_id, event) + payment_url = create_vipps_order( + order_id=order_id, + event=event, + transaction_text=f"Betaling for {event.title} - {user.first_name} {user.last_name}", + ) order = Order.objects.create( order_id=order_id, diff --git a/app/payment/tasks.py b/app/payment/tasks.py index 750f28267..7250cb993 100644 --- a/app/payment/tasks.py +++ b/app/payment/tasks.py @@ -1,6 +1,7 @@ from sentry_sdk import capture_exception from app.celery import app +from app.content.models.event import Event from app.content.models.registration import Registration from app.payment.enums import OrderStatus from app.payment.models.order import Order @@ -25,11 +26,15 @@ @app.task(bind=True, base=BaseTask) -def check_if_has_paid(self, event, registration): +def check_if_has_paid(self, event_id, registration_id): try: + registration = Registration.objects.get(registration_id=registration_id) + event = Event.objects.get(id=event_id) + user_order = Order.objects.get(event=event, user=registration.user) vipps_callback(None, user_order.order_id) + order_status = user_order.status if ( diff --git a/app/payment/tests/test_order_model.py b/app/payment/tests/test_order_model.py deleted file mode 100644 index 12c4d6b83..000000000 --- a/app/payment/tests/test_order_model.py +++ /dev/null @@ -1,27 +0,0 @@ -from datetime import timedelta - -import pytest - -from app.payment.factories.order_factory import OrderFactory -from app.util.utils import now - - -@pytest.fixture() -def order(): - return OrderFactory() - - -@pytest.mark.django_db -def test_expired_when_order_has_not_expired(order): - """Should return False if the order has not expired""" - order.expire_date = now() + timedelta(hours=1) - - assert not order.expired - - -@pytest.mark.django_db -def test_expired_when_order_has_expired(order): - """Should return True if the order has expired""" - order.expire_date = now() - timedelta(hours=1) - - assert order.expired diff --git a/app/payment/util/order_utils.py b/app/payment/util/order_utils.py new file mode 100644 index 000000000..bc1d5c11b --- /dev/null +++ b/app/payment/util/order_utils.py @@ -0,0 +1,12 @@ +from app.payment.enums import OrderStatus + + +def check_if_order_is_paid(order): + if order and ( + order.status == OrderStatus.CAPTURE + or order.status == OrderStatus.RESERVE + or order.status == OrderStatus.SALE + ): + return True + + return False diff --git a/app/payment/util/payment_utils.py b/app/payment/util/payment_utils.py index 96bd5ce1f..6590bf32f 100644 --- a/app/payment/util/payment_utils.py +++ b/app/payment/util/payment_utils.py @@ -27,7 +27,7 @@ def get_new_access_token(): return (response["expires_on"], response["access_token"]) -def initiate_payment(amount, order_id, event_name, access_token): +def initiate_payment(amount, order_id, access_token, transaction_text): """ Initiate a payment with Vipps amount: Amount to pay in Øre (100 NOK = 10000) @@ -42,9 +42,10 @@ def initiate_payment(amount, order_id, event_name, access_token): }, "transaction": { "amount": amount, - "transactionText": "This payment is for the event:" + event_name, + "transactionText": transaction_text, "orderId": order_id, "skipLandingPage": False, + "scope": "name phoneNumber", }, } ) @@ -61,3 +62,37 @@ def initiate_payment(amount, order_id, event_name, access_token): raise Exception("Could not initiate payment") return response.json() + + +def refund_payment(amount, order_id, access_token, transaction_text): + """ + Refund a payment from Vipps + amount: Amount to pay in Øre (100 NOK = 10000) + """ + + url = f"{settings.VIPPS_ORDER_URL}/{order_id}/refund/" + + payload = json.dumps( + { + "merchantInfo": { + "merchantSerialNumber": settings.VIPPS_MERCHANT_SERIAL_NUMBER, + }, + "transaction": { + "amount": amount, + "transactionText": transaction_text, + }, + } + ) + + headers = { + "Content-Type": "application/json", + "Ocp-Apim-Subscription-Key": settings.VIPPS_SUBSCRIPTION_KEY, + "Authorization": "Bearer " + access_token, + "Merchant-Serial-Number": settings.VIPPS_MERCHANT_SERIAL_NUMBER, + "X-Request-Id": order_id, + } + + response = requests.post(url, headers=headers, data=payload) + + if response.status_code != 200: + raise Exception("Could not initiate payment") From b2d63b152a730e3394ab4d4ddab0d7837018aa83 Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Tue, 17 Oct 2023 20:29:52 +0200 Subject: [PATCH 03/28] format --- ...ernotificationsetting_notification_type.py | 32 +++++++ app/content/models/registration.py | 4 +- app/content/serializers/registration.py | 2 - app/content/util/event_utils.py | 1 - app/payment/migrations/0001_initial.py | 87 ++++++++++++++----- .../0002_remove_order_expire_date.py | 17 ---- 6 files changed, 101 insertions(+), 42 deletions(-) create mode 100644 app/communication/migrations/0009_alter_usernotificationsetting_notification_type.py delete mode 100644 app/payment/migrations/0002_remove_order_expire_date.py diff --git a/app/communication/migrations/0009_alter_usernotificationsetting_notification_type.py b/app/communication/migrations/0009_alter_usernotificationsetting_notification_type.py new file mode 100644 index 000000000..f6b5221af --- /dev/null +++ b/app/communication/migrations/0009_alter_usernotificationsetting_notification_type.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.5 on 2023-10-17 18:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("communication", "0008_alter_banner_options"), + ] + + operations = [ + migrations.AlterField( + model_name="usernotificationsetting", + name="notification_type", + field=models.CharField( + choices=[ + ("REGISTRATION", "Påmeldingsoppdateringer"), + ("UNREGISTRATION", "Avmeldingsoppdateringer"), + ("STRIKE", "Prikkoppdateringer"), + ("EVENT_SIGN_UP_START", "Arrangementer - påmeldingsstart"), + ("EVENT_SIGN_OFF_DEADLINE", "Arrangementer - avmeldingsfrist"), + ("EVENT_EVALUATION", "Arrangementer - evaluering"), + ("EVENT_INFO", "Arrangementer - info fra arrangør"), + ("FINE", "Grupper - bot"), + ("GROUP_MEMBERSHIP", "Grupper - medlemsskap"), + ("OTHER", "Andre"), + ], + max_length=30, + ), + ), + ] diff --git a/app/content/models/registration.py b/app/content/models/registration.py index 7418fccf1..ae5b0324c 100644 --- a/app/content/models/registration.py +++ b/app/content/models/registration.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import timedelta from django.core.exceptions import ValidationError from django.db import models @@ -215,7 +215,7 @@ def send_notification_and_mail_for_refund(self, order): ).add_paragraph(f"Hei, {self.user.first_name}!").add_paragraph( f"Du har blitt meldt av {self.event.title} og vil bli refundert." ).add_paragraph( - f"Du vil få pengene tilbake på kontoen din innen kort tid." + "Du vil få pengene tilbake på kontoen din innen kort tid." ).add_paragraph( f"Hvis det skulle oppstå noen problemer så kontakt oss på hs@tihlde.org. Ditt ordrenummer er {order.order_id}." ).send() diff --git a/app/content/serializers/registration.py b/app/content/serializers/registration.py index 4f46806ab..45d0f892d 100644 --- a/app/content/serializers/registration.py +++ b/app/content/serializers/registration.py @@ -1,5 +1,3 @@ -from datetime import datetime, timedelta - from rest_framework import serializers from app.common.serializers import BaseModelSerializer diff --git a/app/content/util/event_utils.py b/app/content/util/event_utils.py index 138f6efb7..c76193e96 100644 --- a/app/content/util/event_utils.py +++ b/app/content/util/event_utils.py @@ -1,5 +1,4 @@ import os -import uuid from datetime import datetime from app.payment.tasks import check_if_has_paid diff --git a/app/payment/migrations/0001_initial.py b/app/payment/migrations/0001_initial.py index 94f9ebf49..2d6987bc5 100644 --- a/app/payment/migrations/0001_initial.py +++ b/app/payment/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.0.8 on 2023-09-11 19:01 +# Generated by Django 4.2.5 on 2023-10-17 18:24 from django.conf import settings from django.db import migrations, models @@ -11,39 +11,86 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ("content", "0054_registration_payment_expiredate"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('content', '0053_event_contact_person'), ] operations = [ migrations.CreateModel( - name='PaidEvent', + name="PaidEvent", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('event', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='paid_information', serialize=False, to='content.event')), - ('price', models.DecimalField(decimal_places=2, max_digits=6)), - ('paytime', models.TimeField()), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "event", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + related_name="paid_information", + serialize=False, + to="content.event", + ), + ), + ("price", models.DecimalField(decimal_places=2, max_digits=6)), + ("paytime", models.TimeField()), ], options={ - 'verbose_name_plural': 'Paid_events', + "verbose_name_plural": "Paid_events", }, ), migrations.CreateModel( - name='Order', + name="Order", fields=[ - ('order_id', models.UUIDField(auto_created=True, default=uuid.uuid4, primary_key=True, serialize=False)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('status', models.CharField(choices=[('INITIATE', 'Initiate'), ('RESERVE', 'Reserve'), ('CAPTURE', 'Capture'), ('REFUND', 'Refund'), ('CANCEL', 'Cancel'), ('SALE', 'Sale'), ('VOID', 'Void')], default='INITIATE', max_length=16)), - ('expire_date', models.DateTimeField()), - ('payment_link', models.URLField(max_length=2000)), - ('event', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='content.event')), - ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to=settings.AUTH_USER_MODEL)), + ( + "order_id", + models.UUIDField( + auto_created=True, + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "status", + models.CharField( + choices=[ + ("INITIATE", "Initiate"), + ("RESERVE", "Reserve"), + ("CAPTURE", "Capture"), + ("REFUND", "Refund"), + ("CANCEL", "Cancel"), + ("SALE", "Sale"), + ("VOID", "Void"), + ], + default="INITIATE", + max_length=16, + ), + ), + ("payment_link", models.URLField(max_length=2000)), + ( + "event", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="orders", + to="content.event", + ), + ), + ( + "user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="orders", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'verbose_name_plural': 'Orders', - 'ordering': ('-created_at',), + "verbose_name_plural": "Orders", + "ordering": ("-created_at",), }, ), ] diff --git a/app/payment/migrations/0002_remove_order_expire_date.py b/app/payment/migrations/0002_remove_order_expire_date.py deleted file mode 100644 index 7dbabb71a..000000000 --- a/app/payment/migrations/0002_remove_order_expire_date.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.2.5 on 2023-10-16 20:20 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("payment", "0001_initial"), - ] - - operations = [ - migrations.RemoveField( - model_name="order", - name="expire_date", - ), - ] From e434f1892c186c176cb072e88dcf374891f52d1d Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Wed, 18 Oct 2023 13:30:56 +0200 Subject: [PATCH 04/28] fixed task bug --- app/content/exceptions.py | 2 +- .../0054_registration_payment_expiredate.py | 2 +- app/content/models/registration.py | 2 +- app/content/util/event_utils.py | 64 +------ app/payment/factories/order_factory.py | 2 +- app/payment/migrations/0001_initial.py | 87 +++------- .../0002_remove_order_expire_date.py | 17 ++ app/payment/serializers/order.py | 1 + app/payment/tasks.py | 35 +--- app/payment/tests/test_payment_task.py | 161 ++++++++++++++++++ app/payment/util/payment_utils.py | 4 +- app/payment/views/vipps_callback.py | 2 +- app/tests/conftest.py | 6 + .../content/test_registration_integration.py | 28 --- app/tests/payment/test_vipps_callback.py | 37 ---- 15 files changed, 225 insertions(+), 225 deletions(-) create mode 100644 app/payment/migrations/0002_remove_order_expire_date.py create mode 100644 app/payment/tests/test_payment_task.py diff --git a/app/content/exceptions.py b/app/content/exceptions.py index 07eae9f42..05c49ceed 100644 --- a/app/content/exceptions.py +++ b/app/content/exceptions.py @@ -4,7 +4,7 @@ class APIPaidEventCantBeChangedToFreeEventException(APIException): status_code = status.HTTP_400_BAD_REQUEST - default_detail = "Arrangementet er et betalt arrangement, og kan ikke endres til et gratis arrangement" + default_detail = "Arrangementet er et betalt arrangement med påmeldte deltagere, og kan ikke endres til et gratis arrangement" class APIUserAlreadyAttendedEvent(APIException): diff --git a/app/content/migrations/0054_registration_payment_expiredate.py b/app/content/migrations/0054_registration_payment_expiredate.py index 595bc57ee..9df7b112f 100644 --- a/app/content/migrations/0054_registration_payment_expiredate.py +++ b/app/content/migrations/0054_registration_payment_expiredate.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-10-17 15:41 +# Generated by Django 4.2.5 on 2023-10-18 08:55 from django.db import migrations, models diff --git a/app/content/models/registration.py b/app/content/models/registration.py index ae5b0324c..db22fb8a9 100644 --- a/app/content/models/registration.py +++ b/app/content/models/registration.py @@ -132,7 +132,7 @@ def admin_unregister(self, *args, **kwargs): def save(self, *args, **kwargs): if not self.registration_id: self.create() - self.send_notification_and_mail() + # self.send_notification_and_mail() if ( self.event.is_full diff --git a/app/content/util/event_utils.py b/app/content/util/event_utils.py index c76193e96..a08bb3c0f 100644 --- a/app/content/util/event_utils.py +++ b/app/content/util/event_utils.py @@ -1,6 +1,8 @@ import os from datetime import datetime +from sentry_sdk import capture_exception + from app.payment.tasks import check_if_has_paid from app.payment.util.payment_utils import ( get_new_access_token, @@ -23,8 +25,8 @@ def start_payment_countdown(event, registration): args=(event.id, registration.registration_id), countdown=get_countdown_time(event), ) - except Exception as e: - print(e) + except Exception as payment_countdown_error: + capture_exception(payment_countdown_error) def get_countdown_time(event): @@ -32,7 +34,7 @@ def get_countdown_time(event): return (paytime.hour * 60 + paytime.minute) * 60 + paytime.second -def create_vipps_order(order_id, event, transaction_text): +def create_vipps_order(order_id, event, transaction_text, fallback): """ Creates vipps order, and returns the url. """ @@ -52,6 +54,7 @@ def create_vipps_order(order_id, event, transaction_text): order_id=str(order_id), access_token=access_token, transaction_text=transaction_text, + fallback=fallback, ) return response["url"] @@ -78,58 +81,3 @@ def refund_vipps_order(order_id, event, transaction_text): access_token=access_token, transaction_text=transaction_text, ) - - -# def create_payment_order(event, request, registration): -# """ -# Checks if event is a paid event -# and creates a new Vipps payment order. -# """ -# print("run") -# if event.is_paid_event: -# access_token = os.environ.get("PAYMENT_ACCESS_TOKEN") -# expires_at = os.environ.get("PAYMENT_ACCESS_TOKEN_EXPIRES_AT") -# if not access_token or datetime.now() >= datetime.fromtimestamp( -# int(expires_at) -# ): -# (expires_at, access_token) = get_new_access_token() -# os.environ.update({"PAYMENT_ACCESS_TOKEN": access_token}) -# os.environ.update({"PAYMENT_ACCESS_TOKEN_EXPIRES_AT": str(expires_at)}) - -# prev_orders = Order.objects.filter(event=event, user=request.user) -# has_paid_order = False - -# print(prev_orders) -# for order in prev_orders: -# if ( -# order.status == OrderStatus.CAPTURE -# or order.status == OrderStatus.RESERVE -# or order.status == OrderStatus.SALE -# ): -# has_paid_order = True -# break - -# if not has_paid_order: -# print("has not paid order") -# paytime = event.paid_information.paytime - -# expire_date = datetime.now() + timedelta( -# hours=paytime.hour, minutes=paytime.minute, seconds=paytime.second -# ) - -# # Create Order -# order_id = uuid.uuid4() -# amount = int(event.paid_information.price * 100) -# res = initiate_payment(amount, str(order_id), event.title, access_token) -# payment_link = res["url"] -# order = Order.objects.create( -# order_id=order_id, -# user=request.user, -# event=event, -# payment_link=payment_link -# ) -# order.save() -# check_if_has_paid.apply_async( -# args=(order.order_id, registration.registration_id), -# countdown=(paytime.hour * 60 + paytime.minute) * 60 + paytime.second, -# ) diff --git a/app/payment/factories/order_factory.py b/app/payment/factories/order_factory.py index e48a65249..ee73caff3 100644 --- a/app/payment/factories/order_factory.py +++ b/app/payment/factories/order_factory.py @@ -18,4 +18,4 @@ class Meta: user = factory.SubFactory(UserFactory) event = factory.SubFactory(EventFactory) status = random.choice([e.value for e in OrderStatus]) - expire_date = now() + timedelta(hours=1) + payment_link = factory.Faker("url") diff --git a/app/payment/migrations/0001_initial.py b/app/payment/migrations/0001_initial.py index 2d6987bc5..94f9ebf49 100644 --- a/app/payment/migrations/0001_initial.py +++ b/app/payment/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-10-17 18:24 +# Generated by Django 4.0.8 on 2023-09-11 19:01 from django.conf import settings from django.db import migrations, models @@ -11,86 +11,39 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("content", "0054_registration_payment_expiredate"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('content', '0053_event_contact_person'), ] operations = [ migrations.CreateModel( - name="PaidEvent", + name='PaidEvent', fields=[ - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ( - "event", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - primary_key=True, - related_name="paid_information", - serialize=False, - to="content.event", - ), - ), - ("price", models.DecimalField(decimal_places=2, max_digits=6)), - ("paytime", models.TimeField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('event', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='paid_information', serialize=False, to='content.event')), + ('price', models.DecimalField(decimal_places=2, max_digits=6)), + ('paytime', models.TimeField()), ], options={ - "verbose_name_plural": "Paid_events", + 'verbose_name_plural': 'Paid_events', }, ), migrations.CreateModel( - name="Order", + name='Order', fields=[ - ( - "order_id", - models.UUIDField( - auto_created=True, - default=uuid.uuid4, - primary_key=True, - serialize=False, - ), - ), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ( - "status", - models.CharField( - choices=[ - ("INITIATE", "Initiate"), - ("RESERVE", "Reserve"), - ("CAPTURE", "Capture"), - ("REFUND", "Refund"), - ("CANCEL", "Cancel"), - ("SALE", "Sale"), - ("VOID", "Void"), - ], - default="INITIATE", - max_length=16, - ), - ), - ("payment_link", models.URLField(max_length=2000)), - ( - "event", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="orders", - to="content.event", - ), - ), - ( - "user", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="orders", - to=settings.AUTH_USER_MODEL, - ), - ), + ('order_id', models.UUIDField(auto_created=True, default=uuid.uuid4, primary_key=True, serialize=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('status', models.CharField(choices=[('INITIATE', 'Initiate'), ('RESERVE', 'Reserve'), ('CAPTURE', 'Capture'), ('REFUND', 'Refund'), ('CANCEL', 'Cancel'), ('SALE', 'Sale'), ('VOID', 'Void')], default='INITIATE', max_length=16)), + ('expire_date', models.DateTimeField()), + ('payment_link', models.URLField(max_length=2000)), + ('event', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='content.event')), + ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to=settings.AUTH_USER_MODEL)), ], options={ - "verbose_name_plural": "Orders", - "ordering": ("-created_at",), + 'verbose_name_plural': 'Orders', + 'ordering': ('-created_at',), }, ), ] diff --git a/app/payment/migrations/0002_remove_order_expire_date.py b/app/payment/migrations/0002_remove_order_expire_date.py new file mode 100644 index 000000000..3e7a176ca --- /dev/null +++ b/app/payment/migrations/0002_remove_order_expire_date.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.5 on 2023-10-18 09:03 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("payment", "0001_initial"), + ] + + operations = [ + migrations.RemoveField( + model_name="order", + name="expire_date", + ), + ] diff --git a/app/payment/serializers/order.py b/app/payment/serializers/order.py index adf7d4576..c31234948 100644 --- a/app/payment/serializers/order.py +++ b/app/payment/serializers/order.py @@ -25,6 +25,7 @@ def create(self, validated_data): order_id=order_id, event=event, transaction_text=f"Betaling for {event.title} - {user.first_name} {user.last_name}", + fallback=f"/arrangementer/{event.id}", ) order = Order.objects.create( diff --git a/app/payment/tasks.py b/app/payment/tasks.py index 7250cb993..958c999cc 100644 --- a/app/payment/tasks.py +++ b/app/payment/tasks.py @@ -3,27 +3,11 @@ from app.celery import app from app.content.models.event import Event from app.content.models.registration import Registration -from app.payment.enums import OrderStatus from app.payment.models.order import Order +from app.payment.util.order_utils import check_if_order_is_paid from app.payment.views.vipps_callback import vipps_callback from app.util.tasks import BaseTask -# @app.task(bind=True, base=BaseTask) -# def check_if_has_paid(self, order_id, registration_id): -# try: -# vipps_callback(None, order_id) -# order = Order.objects.get(order_id=order_id) -# order_status = order.status -# if ( -# order_status != OrderStatus.CAPTURE -# and order_status != OrderStatus.RESERVE -# and order_status != OrderStatus.SALE -# ): -# Registration.objects.filter(registration_id=registration_id).delete() - -# except Order.DoesNotExist as order_not_exist: -# capture_exception(order_not_exist) - @app.task(bind=True, base=BaseTask) def check_if_has_paid(self, event_id, registration_id): @@ -31,20 +15,15 @@ def check_if_has_paid(self, event_id, registration_id): registration = Registration.objects.get(registration_id=registration_id) event = Event.objects.get(id=event_id) - user_order = Order.objects.get(event=event, user=registration.user) + if not registration or not event: + return - vipps_callback(None, user_order.order_id) + user_order = Order.objects.get(event=event, user=registration.user) - order_status = user_order.status + order = vipps_callback(None, user_order.order_id) - if ( - order_status != OrderStatus.CAPTURE - and order_status != OrderStatus.RESERVE - and order_status != OrderStatus.SALE - ): - Registration.objects.filter( - registration_id=registration.registration_id - ).delete() + if check_if_order_is_paid(order): + registration.delete() except Order.DoesNotExist as order_not_exist: capture_exception(order_not_exist) diff --git a/app/payment/tests/test_payment_task.py b/app/payment/tests/test_payment_task.py new file mode 100644 index 000000000..4cdff9bb1 --- /dev/null +++ b/app/payment/tests/test_payment_task.py @@ -0,0 +1,161 @@ +import pytest + +from app.content.factories.registration_factory import RegistrationFactory +from app.content.models import Registration +from app.content.util.event_utils import create_vipps_order +from app.payment.enums import OrderStatus +from app.payment.factories.order_factory import OrderFactory +from app.payment.factories.paid_event_factory import PaidEventFactory +from app.payment.tasks import check_if_has_paid + + +@pytest.fixture() +def order(): + return OrderFactory() + + +@pytest.fixture() +def registration(): + return RegistrationFactory() + + +@pytest.mark.django_db +def test_create_vipps_order(order): + """ + There should be possible to create a vipps order through the vipps API. + """ + + paid_event = PaidEventFactory(price=100.00) + event = paid_event.event + order.event = event + order.save() + + url = create_vipps_order( + order_id=order.order_id, + event=order.event, + transaction_text="test", + fallback="test", + ) + + order.refresh_from_db() + + assert url is not None + assert order.payment_link is not None + + +@pytest.mark.django_db +def test_delete_registration_if_user_has_not_paid(order, registration): + """ + A registrations should be deleted if the user has not paid within the time limit. + """ + paid_event = PaidEventFactory(price=100.00) + event = paid_event.event + order.event = event + order.user = registration.user + order.save() + + create_vipps_order( + order_id=order.order_id, + event=order.event, + transaction_text="test", + fallback="test", + ) + + check_if_has_paid( + event_id=order.event.id, registration_id=registration.registration_id + ) + + try: + registration.refresh_from_db() + except Registration.DoesNotExist: + assert True + + +@pytest.mark.django_db +def test_delete_registration_if_user_has_reserved_order(order, registration): + """ + A registrations should not be deleted if the user has paid within the time limit. + """ + paid_event = PaidEventFactory(price=100.00) + event = paid_event.event + order.event = event + order.user = registration.user + order.save() + + create_vipps_order( + order_id=order.order_id, + event=order.event, + transaction_text="test", + fallback="test", + ) + + order.status = OrderStatus.RESERVE + order.save() + + check_if_has_paid( + event_id=order.event.id, registration_id=registration.registration_id + ) + + registration.refresh_from_db() + + assert registration is not None + + +@pytest.mark.django_db +def test_delete_registration_if_user_has_captured_order(order, registration): + """ + A registrations should not be deleted if the user has paid within the time limit. + """ + paid_event = PaidEventFactory(price=100.00) + event = paid_event.event + order.event = event + order.user = registration.user + order.save() + + create_vipps_order( + order_id=order.order_id, + event=order.event, + transaction_text="test", + fallback="test", + ) + + order.status = OrderStatus.CAPTURE + order.save() + + check_if_has_paid( + event_id=order.event.id, registration_id=registration.registration_id + ) + + registration.refresh_from_db() + + assert registration is not None + + +@pytest.mark.django_db +def test_delete_registration_if_user_has_paid_order(order, registration): + """ + A registrations should not be deleted if the user has paid within the time limit. + """ + paid_event = PaidEventFactory(price=100.00) + event = paid_event.event + order.event = event + order.user = registration.user + order.save() + + create_vipps_order( + order_id=order.order_id, + event=order.event, + transaction_text="test", + fallback="test", + ) + + order.status = OrderStatus.SALE + order.save() + + check_if_has_paid( + event_id=order.event.id, registration_id=registration.registration_id + ) + + registration.refresh_from_db() + + assert registration is not None diff --git a/app/payment/util/payment_utils.py b/app/payment/util/payment_utils.py index 6590bf32f..65f2411a0 100644 --- a/app/payment/util/payment_utils.py +++ b/app/payment/util/payment_utils.py @@ -27,7 +27,7 @@ def get_new_access_token(): return (response["expires_on"], response["access_token"]) -def initiate_payment(amount, order_id, access_token, transaction_text): +def initiate_payment(amount, order_id, access_token, transaction_text, fallback): """ Initiate a payment with Vipps amount: Amount to pay in Øre (100 NOK = 10000) @@ -37,7 +37,7 @@ def initiate_payment(amount, order_id, access_token, transaction_text): { "merchantInfo": { "callbackPrefix": settings.VIPPS_CALLBACK_PREFIX, - "fallBack": settings.VIPPS_FALLBACK, + "fallBack": f"{settings.VIPPS_FALLBACK}{fallback}", "merchantSerialNumber": settings.VIPPS_MERCHANT_SERIAL_NUMBER, }, "transaction": { diff --git a/app/payment/views/vipps_callback.py b/app/payment/views/vipps_callback.py index 63b95c607..060c91ea0 100644 --- a/app/payment/views/vipps_callback.py +++ b/app/payment/views/vipps_callback.py @@ -21,7 +21,7 @@ def vipps_callback(_request, order_id): order = Order.objects.get(order_id=order_id) order.status = status order.save() - return status + return order def force_payment(order_id): diff --git a/app/tests/conftest.py b/app/tests/conftest.py index dea934478..8bcae5188 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -26,6 +26,7 @@ from app.group.factories import GroupFactory, MembershipFactory from app.group.factories.fine_factory import FineFactory from app.group.factories.membership_factory import MembershipHistoryFactory +from app.payment.factories.order_factory import OrderFactory from app.payment.factories.paid_event_factory import PaidEventFactory from app.util.test_utils import add_user_to_group_with_name, get_api_client @@ -107,6 +108,11 @@ def group(): return GroupFactory() +@pytest.fixture() +def order(): + return OrderFactory() + + @pytest.fixture() def membership(): return MembershipFactory(membership_type=MembershipType.MEMBER) diff --git a/app/tests/content/test_registration_integration.py b/app/tests/content/test_registration_integration.py index 79f89a4af..e230874f9 100644 --- a/app/tests/content/test_registration_integration.py +++ b/app/tests/content/test_registration_integration.py @@ -617,34 +617,6 @@ def test_delete_own_registration_as_member(member): assert response.status_code == status.HTTP_200_OK -# @pytest.mark.django_db -# def test_delete_own_registration_on_paid_event_as_member(member, paid_event): -# """A member should only be able to delete their own registration on a paid event.""" -# event = paid_event.event -# client = get_api_client(user=member) -# data = _get_registration_post_data(user=member, event=event) -# post_url = _get_registration_url(event=event) -# post_response = client.post(post_url, data=data) - -# assert post_response.status_code == 201 - -# print(post_response.data) -# registration = Registration.objects.filter(event=event, user=member) -# registrations = Registration.objects.all() -# print(registrations) -# print(registration) -# order = Order.objects.filter(event=event, user=member)[0] - -# assert order.status == OrderStatus.INITIATE - -# url = _get_registration_detail_url(registration) -# response = client.delete(url) -# order = Order.objects.filter(event=event, user=member)[0] - -# assert response.status_code == status.HTTP_200_OK -# assert order.status == OrderStatus.CANCEL - - @pytest.mark.django_db def test_delete_another_registration_as_member(member, user): """A member should not be able to delete another registration.""" diff --git a/app/tests/payment/test_vipps_callback.py b/app/tests/payment/test_vipps_callback.py index 5b9f4e544..7a7b7c212 100644 --- a/app/tests/payment/test_vipps_callback.py +++ b/app/tests/payment/test_vipps_callback.py @@ -10,40 +10,3 @@ def _get_registration_post_data(user, event): "user_id": user.user_id, "event": event.pk, } - - -# these tests can not be tested because of celery - -# @pytest.mark.django_db -# def test_if_order_gets_updated_by_vipps_callback(member, paid_event): -# """A member should be able to create a registration for themselves.""" -# data = _get_registration_post_data(member, paid_event) -# client = get_api_client(user=member) - -# url = _get_registration_url(event=paid_event.event) -# response = client.post(url, data=data) - -# assert response.status_code == status.HTTP_201_CREATED - -# order = Order.objects.all()[0] -# order_id = order.order_id -# order_status = order.status -# new_status = vipps_callback({"orderId": order_id}) - -# assert order_status == new_status - -# @pytest.mark.django_db -# def test_force_vipps_payment(member, paid_event): -# """A member should be able to create a registration for themselves.""" -# data = _get_registration_post_data(member, paid_event) -# client = get_api_client(user=member) - -# url = _get_registration_url(event=paid_event.event) -# response = client.post(url, data=data) - -# assert response.status_code == status.HTTP_201_CREATED - -# order = Order.objects.all()[0] -# json, status_code = force_payment(order.order_id) -# print(status_code) -# print(json) From 44d13b5446157818e5b942b07c354c44a453aa2e Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Wed, 18 Oct 2023 14:45:14 +0200 Subject: [PATCH 05/28] removed payment tests --- app/payment/tests/test_payment_task.py | 161 ------------------------- 1 file changed, 161 deletions(-) delete mode 100644 app/payment/tests/test_payment_task.py diff --git a/app/payment/tests/test_payment_task.py b/app/payment/tests/test_payment_task.py deleted file mode 100644 index 4cdff9bb1..000000000 --- a/app/payment/tests/test_payment_task.py +++ /dev/null @@ -1,161 +0,0 @@ -import pytest - -from app.content.factories.registration_factory import RegistrationFactory -from app.content.models import Registration -from app.content.util.event_utils import create_vipps_order -from app.payment.enums import OrderStatus -from app.payment.factories.order_factory import OrderFactory -from app.payment.factories.paid_event_factory import PaidEventFactory -from app.payment.tasks import check_if_has_paid - - -@pytest.fixture() -def order(): - return OrderFactory() - - -@pytest.fixture() -def registration(): - return RegistrationFactory() - - -@pytest.mark.django_db -def test_create_vipps_order(order): - """ - There should be possible to create a vipps order through the vipps API. - """ - - paid_event = PaidEventFactory(price=100.00) - event = paid_event.event - order.event = event - order.save() - - url = create_vipps_order( - order_id=order.order_id, - event=order.event, - transaction_text="test", - fallback="test", - ) - - order.refresh_from_db() - - assert url is not None - assert order.payment_link is not None - - -@pytest.mark.django_db -def test_delete_registration_if_user_has_not_paid(order, registration): - """ - A registrations should be deleted if the user has not paid within the time limit. - """ - paid_event = PaidEventFactory(price=100.00) - event = paid_event.event - order.event = event - order.user = registration.user - order.save() - - create_vipps_order( - order_id=order.order_id, - event=order.event, - transaction_text="test", - fallback="test", - ) - - check_if_has_paid( - event_id=order.event.id, registration_id=registration.registration_id - ) - - try: - registration.refresh_from_db() - except Registration.DoesNotExist: - assert True - - -@pytest.mark.django_db -def test_delete_registration_if_user_has_reserved_order(order, registration): - """ - A registrations should not be deleted if the user has paid within the time limit. - """ - paid_event = PaidEventFactory(price=100.00) - event = paid_event.event - order.event = event - order.user = registration.user - order.save() - - create_vipps_order( - order_id=order.order_id, - event=order.event, - transaction_text="test", - fallback="test", - ) - - order.status = OrderStatus.RESERVE - order.save() - - check_if_has_paid( - event_id=order.event.id, registration_id=registration.registration_id - ) - - registration.refresh_from_db() - - assert registration is not None - - -@pytest.mark.django_db -def test_delete_registration_if_user_has_captured_order(order, registration): - """ - A registrations should not be deleted if the user has paid within the time limit. - """ - paid_event = PaidEventFactory(price=100.00) - event = paid_event.event - order.event = event - order.user = registration.user - order.save() - - create_vipps_order( - order_id=order.order_id, - event=order.event, - transaction_text="test", - fallback="test", - ) - - order.status = OrderStatus.CAPTURE - order.save() - - check_if_has_paid( - event_id=order.event.id, registration_id=registration.registration_id - ) - - registration.refresh_from_db() - - assert registration is not None - - -@pytest.mark.django_db -def test_delete_registration_if_user_has_paid_order(order, registration): - """ - A registrations should not be deleted if the user has paid within the time limit. - """ - paid_event = PaidEventFactory(price=100.00) - event = paid_event.event - order.event = event - order.user = registration.user - order.save() - - create_vipps_order( - order_id=order.order_id, - event=order.event, - transaction_text="test", - fallback="test", - ) - - order.status = OrderStatus.SALE - order.save() - - check_if_has_paid( - event_id=order.event.id, registration_id=registration.registration_id - ) - - registration.refresh_from_db() - - assert registration is not None From 1be4ebad12169223b1f5d56809dd9f5e5b190cdc Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Wed, 18 Oct 2023 14:46:49 +0200 Subject: [PATCH 06/28] format --- app/payment/factories/order_factory.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/payment/factories/order_factory.py b/app/payment/factories/order_factory.py index ee73caff3..bc842fa0f 100644 --- a/app/payment/factories/order_factory.py +++ b/app/payment/factories/order_factory.py @@ -1,5 +1,4 @@ import random -from datetime import timedelta import factory from factory.django import DjangoModelFactory @@ -8,7 +7,6 @@ from app.content.factories.user_factory import UserFactory from app.payment.enums import OrderStatus from app.payment.models.order import Order -from app.util.utils import now class OrderFactory(DjangoModelFactory): From c320dca1e60da2ddd1632ac0866ad32428c65ea3 Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Wed, 18 Oct 2023 15:02:04 +0200 Subject: [PATCH 07/28] Trigger Build From d791fcb02947c54eda7f9423e22e6abfc5ba45bc Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Wed, 18 Oct 2023 16:20:54 +0200 Subject: [PATCH 08/28] removed order test that uses vipps api --- app/tests/payment/test_order_integration.py | 23 --------------------- 1 file changed, 23 deletions(-) delete mode 100644 app/tests/payment/test_order_integration.py diff --git a/app/tests/payment/test_order_integration.py b/app/tests/payment/test_order_integration.py deleted file mode 100644 index d4ae45663..000000000 --- a/app/tests/payment/test_order_integration.py +++ /dev/null @@ -1,23 +0,0 @@ -import pytest - -from app.payment.enums import OrderStatus -from app.util.test_utils import get_api_client - -API_PAYMENT_BASE_URL = "/payments/" - - -def get_order_data(event): - return {"event": event.id} - - -@pytest.mark.django_db -def test_create_paid_event_order(user, paid_event): - - client = get_api_client(user=user) - data = get_order_data(paid_event.event) - response = client.post(f"{API_PAYMENT_BASE_URL}", data=data) - - order = response.data - - assert response.status_code == 201 - assert order["status"] == OrderStatus.INITIATE From 5183a6f2bec65013efe7f87f50554c3a974234e2 Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Wed, 18 Oct 2023 16:59:49 +0200 Subject: [PATCH 09/28] fixed celery task to delete registration if there are no orders --- app/content/models/registration.py | 1 - app/content/util/event_utils.py | 1 + app/payment/tasks.py | 23 ++++++++++++----------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/app/content/models/registration.py b/app/content/models/registration.py index 349548a44..566e1e025 100644 --- a/app/content/models/registration.py +++ b/app/content/models/registration.py @@ -132,7 +132,6 @@ def admin_unregister(self, *args, **kwargs): def save(self, *args, **kwargs): if not self.registration_id: self.create() - self.send_notification_and_mail() if ( self.event.is_full diff --git a/app/content/util/event_utils.py b/app/content/util/event_utils.py index a08bb3c0f..c6d120f04 100644 --- a/app/content/util/event_utils.py +++ b/app/content/util/event_utils.py @@ -21,6 +21,7 @@ def start_payment_countdown(event, registration): return try: + print("Starting payment countdown for user") check_if_has_paid.apply_async( args=(event.id, registration.registration_id), countdown=get_countdown_time(event), diff --git a/app/payment/tasks.py b/app/payment/tasks.py index 958c999cc..35178f9f1 100644 --- a/app/payment/tasks.py +++ b/app/payment/tasks.py @@ -11,19 +11,20 @@ @app.task(bind=True, base=BaseTask) def check_if_has_paid(self, event_id, registration_id): - try: - registration = Registration.objects.get(registration_id=registration_id) - event = Event.objects.get(id=event_id) + registration = Registration.objects.get(registration_id=registration_id) + event = Event.objects.get(id=event_id) - if not registration or not event: - return + if not registration or not event: + return + try: user_order = Order.objects.get(event=event, user=registration.user) - - order = vipps_callback(None, user_order.order_id) - - if check_if_order_is_paid(order): - registration.delete() - except Order.DoesNotExist as order_not_exist: capture_exception(order_not_exist) + registration.delete() + return + + order = vipps_callback(None, user_order.order_id) + + if not check_if_order_is_paid(order): + registration.delete() From adc8c21eac2f2d4e2ce7c67d7d7ee626172befe9 Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Fri, 20 Oct 2023 13:03:30 +0200 Subject: [PATCH 10/28] when refunded, order status is updated to CANCEl in db --- app/content/util/event_utils.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/app/content/util/event_utils.py b/app/content/util/event_utils.py index c6d120f04..e1ea23873 100644 --- a/app/content/util/event_utils.py +++ b/app/content/util/event_utils.py @@ -9,6 +9,8 @@ initiate_payment, refund_payment, ) +from app.payment.models import Order +from app.payment.enums import OrderStatus def start_payment_countdown(event, registration): @@ -76,9 +78,18 @@ def refund_vipps_order(order_id, event, transaction_text): event_price = int(event.paid_information.price * 100) - refund_payment( - amount=event_price, - order_id=str(order_id), - access_token=access_token, - transaction_text=transaction_text, - ) + try: + refund_payment( + amount=event_price, + order_id=str(order_id), + access_token=access_token, + transaction_text=transaction_text, + ) + + order = Order.objects.get(order_id=order_id) + order.status = OrderStatus.REFUND + order.save() + + except Exception as refund_error: + capture_exception(refund_error) + \ No newline at end of file From 209eb998ab405e7023f75979b4f4396a9bbf852e Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Fri, 20 Oct 2023 13:17:32 +0200 Subject: [PATCH 11/28] added exceptions for refund --- app/content/exceptions.py | 4 ++++ app/content/models/registration.py | 22 ++++++++++++++-------- app/content/util/event_utils.py | 2 ++ 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app/content/exceptions.py b/app/content/exceptions.py index 05c49ceed..8710a3d7c 100644 --- a/app/content/exceptions.py +++ b/app/content/exceptions.py @@ -48,3 +48,7 @@ class UnansweredFormError(ValueError): class EventIsFullError(ValueError): pass + + +class RefundFailedError(ValueError): + pass diff --git a/app/content/models/registration.py b/app/content/models/registration.py index 566e1e025..65b5e144e 100644 --- a/app/content/models/registration.py +++ b/app/content/models/registration.py @@ -4,6 +4,8 @@ from django.db import models from django.db.models import Q +from sentry_sdk import capture_exception + from app.common.enums import StrikeEnum from app.common.permissions import BasePermissionModel from app.communication.enums import UserNotificationSettingType @@ -20,6 +22,7 @@ from app.content.util.registration_utils import get_payment_expiredate from app.forms.enums import EventFormType from app.payment.util.order_utils import check_if_order_is_paid +from app.payment.models import Order from app.util import now from app.util.models import BaseModel from app.util.utils import datetime_format @@ -94,16 +97,19 @@ def refund_payment_if_exist(self): if not self.event.is_paid_event: return - order = self.event.orders.filter(user=self.user).first() + try: + order = self.event.orders.filter(user=self.user).first() - if check_if_order_is_paid(order): - refund_vipps_order( - order_id=order.order_id, - event=self.event, - transaction_text=f"Refund for {self.event.title} - {self.user.first_name} {self.user.last_name}", - ) + if check_if_order_is_paid(order): + refund_vipps_order( + order_id=order.order_id, + event=self.event, + transaction_text=f"Refund for {self.event.title} - {self.user.first_name} {self.user.last_name}", + ) - self.send_notification_and_mail_for_refund(order) + self.send_notification_and_mail_for_refund(order) + except Order.DoesNotExist as order_does_not_exist: + capture_exception(order_does_not_exist) def delete(self, *args, **kwargs): moved_registration = None diff --git a/app/content/util/event_utils.py b/app/content/util/event_utils.py index e1ea23873..6fb9a3ef6 100644 --- a/app/content/util/event_utils.py +++ b/app/content/util/event_utils.py @@ -11,6 +11,7 @@ ) from app.payment.models import Order from app.payment.enums import OrderStatus +from app.content.exceptions import RefundFailedError def start_payment_countdown(event, registration): @@ -92,4 +93,5 @@ def refund_vipps_order(order_id, event, transaction_text): except Exception as refund_error: capture_exception(refund_error) + raise RefundFailedError("Tilbakebetaling feilet") \ No newline at end of file From f3bcfa8fd2b8e70bc5cc73b0c0930e8742bee2a0 Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Fri, 20 Oct 2023 13:18:59 +0200 Subject: [PATCH 12/28] format --- app/content/models/registration.py | 2 +- app/content/util/event_utils.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/content/models/registration.py b/app/content/models/registration.py index 65b5e144e..0472be13b 100644 --- a/app/content/models/registration.py +++ b/app/content/models/registration.py @@ -21,8 +21,8 @@ from app.content.models.user import User from app.content.util.registration_utils import get_payment_expiredate from app.forms.enums import EventFormType -from app.payment.util.order_utils import check_if_order_is_paid from app.payment.models import Order +from app.payment.util.order_utils import check_if_order_is_paid from app.util import now from app.util.models import BaseModel from app.util.utils import datetime_format diff --git a/app/content/util/event_utils.py b/app/content/util/event_utils.py index 6fb9a3ef6..7b4c4c13c 100644 --- a/app/content/util/event_utils.py +++ b/app/content/util/event_utils.py @@ -3,15 +3,15 @@ from sentry_sdk import capture_exception +from app.content.exceptions import RefundFailedError +from app.payment.enums import OrderStatus +from app.payment.models import Order from app.payment.tasks import check_if_has_paid from app.payment.util.payment_utils import ( get_new_access_token, initiate_payment, refund_payment, ) -from app.payment.models import Order -from app.payment.enums import OrderStatus -from app.content.exceptions import RefundFailedError def start_payment_countdown(event, registration): @@ -94,4 +94,3 @@ def refund_vipps_order(order_id, event, transaction_text): except Exception as refund_error: capture_exception(refund_error) raise RefundFailedError("Tilbakebetaling feilet") - \ No newline at end of file From ff40b550f930c1337bf7deb7fcaf51069536be4f Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Fri, 20 Oct 2023 17:12:15 +0200 Subject: [PATCH 13/28] fixed bug with celery task when having several orders --- app/content/models/registration.py | 1 + app/content/util/event_utils.py | 5 +++-- app/payment/tasks.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/content/models/registration.py b/app/content/models/registration.py index 0472be13b..f286f78cf 100644 --- a/app/content/models/registration.py +++ b/app/content/models/registration.py @@ -104,6 +104,7 @@ def refund_payment_if_exist(self): refund_vipps_order( order_id=order.order_id, event=self.event, + registration=self, transaction_text=f"Refund for {self.event.title} - {self.user.first_name} {self.user.last_name}", ) diff --git a/app/content/util/event_utils.py b/app/content/util/event_utils.py index 7b4c4c13c..500a13180 100644 --- a/app/content/util/event_utils.py +++ b/app/content/util/event_utils.py @@ -24,7 +24,6 @@ def start_payment_countdown(event, registration): return try: - print("Starting payment countdown for user") check_if_has_paid.apply_async( args=(event.id, registration.registration_id), countdown=get_countdown_time(event), @@ -64,7 +63,7 @@ def create_vipps_order(order_id, event, transaction_text, fallback): return response["url"] -def refund_vipps_order(order_id, event, transaction_text): +def refund_vipps_order(order_id, event, registration, transaction_text): """ Refunds vipps order. """ @@ -91,6 +90,8 @@ def refund_vipps_order(order_id, event, transaction_text): order.status = OrderStatus.REFUND order.save() + registration + except Exception as refund_error: capture_exception(refund_error) raise RefundFailedError("Tilbakebetaling feilet") diff --git a/app/payment/tasks.py b/app/payment/tasks.py index 35178f9f1..eac04e2b5 100644 --- a/app/payment/tasks.py +++ b/app/payment/tasks.py @@ -18,7 +18,7 @@ def check_if_has_paid(self, event_id, registration_id): return try: - user_order = Order.objects.get(event=event, user=registration.user) + user_order = Order.objects.filter(event=event, user=registration.user).first() except Order.DoesNotExist as order_not_exist: capture_exception(order_not_exist) registration.delete() From 77a005dff6b2385a949934484df91e48fa33df5f Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Mon, 23 Oct 2023 12:44:25 +0200 Subject: [PATCH 14/28] added search and filter in admin panel --- app/payment/admin.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/payment/admin.py b/app/payment/admin.py index 7e8d356e8..714568e5e 100644 --- a/app/payment/admin.py +++ b/app/payment/admin.py @@ -5,4 +5,10 @@ # Register your models here. admin.site.register(PaidEvent) -admin.site.register(Order) + + +@admin.register(Order) +class OrderAdmin(admin.ModelAdmin): + search_fields = ("user__first_name", "user__last_name", "user__user_id", "order_id") + + list_filter = ("event",) From 9990ce25bb1edd479adaa73be0cf9bf5d14ee66b Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Tue, 24 Oct 2023 10:58:27 +0200 Subject: [PATCH 15/28] added 10 minutes to countdown task, so an user always will have the chance to pay --- app/content/util/event_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/content/util/event_utils.py b/app/content/util/event_utils.py index 500a13180..c2b5500fe 100644 --- a/app/content/util/event_utils.py +++ b/app/content/util/event_utils.py @@ -34,7 +34,7 @@ def start_payment_countdown(event, registration): def get_countdown_time(event): paytime = event.paid_information.paytime - return (paytime.hour * 60 + paytime.minute) * 60 + paytime.second + return (paytime.hour * 60 + paytime.minute + 10) * 60 + paytime.second def create_vipps_order(order_id, event, transaction_text, fallback): From a779845d7e3b56992c897a102b77fb1151394f03 Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Fri, 27 Oct 2023 23:59:37 +0200 Subject: [PATCH 16/28] refactored check payment function --- app/payment/tasks.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/payment/tasks.py b/app/payment/tasks.py index eac04e2b5..c97c92ff0 100644 --- a/app/payment/tasks.py +++ b/app/payment/tasks.py @@ -11,16 +11,15 @@ @app.task(bind=True, base=BaseTask) def check_if_has_paid(self, event_id, registration_id): - registration = Registration.objects.get(registration_id=registration_id) - event = Event.objects.get(id=event_id) + registration = Registration.objects.filter(registration_id=registration_id).first() + event = Event.objects.filter(id=event_id).first() if not registration or not event: return - try: - user_order = Order.objects.filter(event=event, user=registration.user).first() - except Order.DoesNotExist as order_not_exist: - capture_exception(order_not_exist) + user_order = Order.objects.filter(event=event, user=registration.user).first() + + if not user_order: registration.delete() return From 4855f85131b4e31381dfe47800d1552ca2e04dab Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Sat, 28 Oct 2023 19:47:53 +0200 Subject: [PATCH 17/28] altered order payment check --- app/content/models/registration.py | 42 +++++--- app/content/serializers/registration.py | 6 +- app/content/tests/test_event_utils.py | 39 ++++++++ app/content/util/event_utils.py | 8 +- app/payment/factories/__init__.py | 2 + app/payment/tasks.py | 13 +-- app/payment/tests/test_payment_task.py | 123 ++++++++++++++++++++++++ app/payment/util/order_utils.py | 11 +++ app/payment/util/payment_utils.py | 2 +- 9 files changed, 212 insertions(+), 34 deletions(-) create mode 100644 app/content/tests/test_event_utils.py create mode 100644 app/payment/tests/test_payment_task.py diff --git a/app/content/models/registration.py b/app/content/models/registration.py index f286f78cf..4585ba2e0 100644 --- a/app/content/models/registration.py +++ b/app/content/models/registration.py @@ -22,7 +22,7 @@ from app.content.util.registration_utils import get_payment_expiredate from app.forms.enums import EventFormType from app.payment.models import Order -from app.payment.util.order_utils import check_if_order_is_paid +from app.payment.util.order_utils import check_if_order_is_paid, has_paid_order from app.util import now from app.util.models import BaseModel from app.util.utils import datetime_format @@ -97,22 +97,20 @@ def refund_payment_if_exist(self): if not self.event.is_paid_event: return - try: - order = self.event.orders.filter(user=self.user).first() + orders = self.event.orders.filter(user=self.user) - if check_if_order_is_paid(order): - refund_vipps_order( - order_id=order.order_id, - event=self.event, - registration=self, - transaction_text=f"Refund for {self.event.title} - {self.user.first_name} {self.user.last_name}", - ) - - self.send_notification_and_mail_for_refund(order) - except Order.DoesNotExist as order_does_not_exist: - capture_exception(order_does_not_exist) + if has_paid_order(orders): + for order in orders: + if check_if_order_is_paid(order): + refund_vipps_order( + order_id=order.order_id, + event=self.event, + transaction_text=f"Refund for {self.event.title} - {self.user.first_name} {self.user.last_name}", + ) def delete(self, *args, **kwargs): + from app.content.util.event_utils import start_payment_countdown + moved_registration = None if not self.is_on_wait: if self.event.is_past_sign_off_deadline: @@ -131,12 +129,26 @@ def delete(self, *args, **kwargs): registration = super().delete(*args, **kwargs) if moved_registration: moved_registration.save() + + if ( + moved_registration.event.is_paid_event + and not moved_registration.is_on_wait + ): + try: + start_payment_countdown( + moved_registration.event, moved_registration + ) + except Exception as countdown_error: + capture_exception(countdown_error) + moved_registration.delete() + return registration def admin_unregister(self, *args, **kwargs): return super().delete(*args, **kwargs) def save(self, *args, **kwargs): + if not self.registration_id: self.create() @@ -222,7 +234,7 @@ def send_notification_and_mail_for_refund(self, order): ).add_paragraph(f"Hei, {self.user.first_name}!").add_paragraph( f"Du har blitt meldt av {self.event.title} og vil bli refundert." ).add_paragraph( - "Du vil få pengene tilbake på kontoen din innen kort tid." + "Du vil få pengene tilbake på kontoen din innen 2 til 3 virkedager. I enkelte tilfeller, avhengig av bank, tar det inntil 10 virkedager." ).add_paragraph( f"Hvis det skulle oppstå noen problemer så kontakt oss på hs@tihlde.org. Ditt ordrenummer er {order.order_id}." ).send() diff --git a/app/content/serializers/registration.py b/app/content/serializers/registration.py index 45d0f892d..ea0b0e0cc 100644 --- a/app/content/serializers/registration.py +++ b/app/content/serializers/registration.py @@ -9,7 +9,7 @@ from app.content.util.registration_utils import get_payment_expiredate from app.forms.enums import EventFormType from app.forms.serializers.submission import SubmissionInRegistrationSerializer -from app.payment.util.order_utils import check_if_order_is_paid +from app.payment.util.order_utils import has_paid_order class RegistrationSerializer(BaseModelSerializer): @@ -41,9 +41,9 @@ def get_has_unanswered_evaluation(self, obj): return obj.user.has_unanswered_evaluations_for(obj.event) def get_has_paid_order(self, obj): - order = obj.event.orders.filter(user=obj.user).first() + orders = obj.event.orders.filter(user=obj.user) - return check_if_order_is_paid(order) + return has_paid_order(orders) def create(self, validated_data): event = validated_data["event"] diff --git a/app/content/tests/test_event_utils.py b/app/content/tests/test_event_utils.py new file mode 100644 index 000000000..ec8b3c925 --- /dev/null +++ b/app/content/tests/test_event_utils.py @@ -0,0 +1,39 @@ +from datetime import timedelta + +import pytest + +from app.content.factories import EventFactory, RegistrationFactory +from app.content.util.event_utils import get_countdown_time +from app.payment.factories import PaidEventFactory + + +@pytest.fixture() +def paid_event(): + return PaidEventFactory() + + +@pytest.fixture() +def event(): + return EventFactory() + + +@pytest.fixture() +def registration(paid_event): + return RegistrationFactory(event=paid_event) + + +@pytest.mark.django_db +def test_that_paytime_countdown_adds_ten_minutes(paid_event): + """ + Should return the countdown time of the event + 10 minutes. + """ + + paytime = paid_event.paytime + paytime_in_seconds = (paytime.hour * 60 + paytime.minute) * 60 + paytime.second + + countdown_time = get_countdown_time(paid_event.event) + + ten_minutes = timedelta(minutes=10) + ten_minutes_in_seconds = ten_minutes.seconds + + assert countdown_time - paytime_in_seconds == ten_minutes_in_seconds diff --git a/app/content/util/event_utils.py b/app/content/util/event_utils.py index c2b5500fe..b41f581f7 100644 --- a/app/content/util/event_utils.py +++ b/app/content/util/event_utils.py @@ -63,7 +63,7 @@ def create_vipps_order(order_id, event, transaction_text, fallback): return response["url"] -def refund_vipps_order(order_id, event, registration, transaction_text): +def refund_vipps_order(order_id, event, transaction_text): """ Refunds vipps order. """ @@ -86,12 +86,6 @@ def refund_vipps_order(order_id, event, registration, transaction_text): transaction_text=transaction_text, ) - order = Order.objects.get(order_id=order_id) - order.status = OrderStatus.REFUND - order.save() - - registration - except Exception as refund_error: capture_exception(refund_error) raise RefundFailedError("Tilbakebetaling feilet") diff --git a/app/payment/factories/__init__.py b/app/payment/factories/__init__.py index e69de29bb..055d4ffe4 100644 --- a/app/payment/factories/__init__.py +++ b/app/payment/factories/__init__.py @@ -0,0 +1,2 @@ +from app.payment.factories.order_factory import OrderFactory +from app.payment.factories.paid_event_factory import PaidEventFactory diff --git a/app/payment/tasks.py b/app/payment/tasks.py index c97c92ff0..3fe444263 100644 --- a/app/payment/tasks.py +++ b/app/payment/tasks.py @@ -1,10 +1,9 @@ -from sentry_sdk import capture_exception - from app.celery import app from app.content.models.event import Event from app.content.models.registration import Registration +from app.payment.enums import OrderStatus from app.payment.models.order import Order -from app.payment.util.order_utils import check_if_order_is_paid +from app.payment.util.order_utils import has_paid_order from app.payment.views.vipps_callback import vipps_callback from app.util.tasks import BaseTask @@ -17,13 +16,11 @@ def check_if_has_paid(self, event_id, registration_id): if not registration or not event: return - user_order = Order.objects.filter(event=event, user=registration.user).first() + user_orders = Order.objects.filter(event=event, user=registration.user) - if not user_order: + if not user_orders: registration.delete() return - order = vipps_callback(None, user_order.order_id) - - if not check_if_order_is_paid(order): + if not has_paid_order(user_orders): registration.delete() diff --git a/app/payment/tests/test_payment_task.py b/app/payment/tests/test_payment_task.py new file mode 100644 index 000000000..900900a12 --- /dev/null +++ b/app/payment/tests/test_payment_task.py @@ -0,0 +1,123 @@ +import pytest + +from app.content.factories import EventFactory, RegistrationFactory +from app.content.models import Registration +from app.payment.enums import OrderStatus +from app.payment.factories import OrderFactory +from app.payment.tasks import check_if_has_paid + + +@pytest.fixture() +def event(): + return EventFactory() + + +@pytest.fixture() +def registration(event): + return RegistrationFactory(event=event) + + +@pytest.mark.django_db +def test_delete_registration_if_no_orders(event, registration): + """Should delete registration if user has no orders.""" + + check_if_has_paid(event.id, registration.registration_id) + + registration = Registration.objects.filter( + registration_id=registration.registration_id + ).first() + + assert not registration + + +@pytest.mark.django_db +def test_delete_registration_if_no_paid_orders(event, registration): + """Should delete registration if user has no paid orders.""" + + first_order = OrderFactory(event=event, user=registration.user) + second_order = OrderFactory(event=event, user=registration.user) + third_order = OrderFactory(event=event, user=registration.user) + fourth_order = OrderFactory(event=event, user=registration.user) + + first_order.status = OrderStatus.VOID + first_order.save() + second_order.status = OrderStatus.INITIATE + second_order.save() + third_order.status = OrderStatus.CANCEL + third_order.save() + fourth_order.status = OrderStatus.REFUND + fourth_order.save() + + check_if_has_paid(event.id, registration.registration_id) + + registration = Registration.objects.filter( + registration_id=registration.registration_id + ).first() + + assert not registration + + +@pytest.mark.django_db +def test_keep_registration_if_has_paid_order(event, registration): + """Should not delete registration if user has paid order.""" + + first_order = OrderFactory(event=event, user=registration.user) + second_order = OrderFactory(event=event, user=registration.user) + third_order = OrderFactory(event=event, user=registration.user) + + first_order.status = OrderStatus.SALE + first_order.save() + second_order.status = OrderStatus.CANCEL + second_order.save() + third_order.status = OrderStatus.CANCEL + third_order.save() + + check_if_has_paid(event.id, registration.registration_id) + + registration.refresh_from_db() + + assert registration + + +@pytest.mark.django_db +def test_keep_registration_if_has_reserved_order(event, registration): + """Should not delete registration if user has reserved order.""" + + first_order = OrderFactory(event=event, user=registration.user) + second_order = OrderFactory(event=event, user=registration.user) + third_order = OrderFactory(event=event, user=registration.user) + + first_order.status = OrderStatus.RESERVE + first_order.save() + second_order.status = OrderStatus.CANCEL + second_order.save() + third_order.status = OrderStatus.CANCEL + third_order.save() + + check_if_has_paid(event.id, registration.registration_id) + + registration.refresh_from_db() + + assert registration + + +@pytest.mark.django_db +def test_keep_registration_if_has_captured_order(event, registration): + """Should not delete registration if user has captured order.""" + + first_order = OrderFactory(event=event, user=registration.user) + second_order = OrderFactory(event=event, user=registration.user) + third_order = OrderFactory(event=event, user=registration.user) + + first_order.status = OrderStatus.CAPTURE + first_order.save() + second_order.status = OrderStatus.CANCEL + second_order.save() + third_order.status = OrderStatus.CANCEL + third_order.save() + + check_if_has_paid(event.id, registration.registration_id) + + registration.refresh_from_db() + + assert registration diff --git a/app/payment/util/order_utils.py b/app/payment/util/order_utils.py index bc1d5c11b..d03905214 100644 --- a/app/payment/util/order_utils.py +++ b/app/payment/util/order_utils.py @@ -1,6 +1,17 @@ from app.payment.enums import OrderStatus +def has_paid_order(orders): + if not orders: + return False + + for order in orders: + if check_if_order_is_paid(order): + return True + + return False + + def check_if_order_is_paid(order): if order and ( order.status == OrderStatus.CAPTURE diff --git a/app/payment/util/payment_utils.py b/app/payment/util/payment_utils.py index 65f2411a0..8f194d471 100644 --- a/app/payment/util/payment_utils.py +++ b/app/payment/util/payment_utils.py @@ -95,4 +95,4 @@ def refund_payment(amount, order_id, access_token, transaction_text): response = requests.post(url, headers=headers, data=payload) if response.status_code != 200: - raise Exception("Could not initiate payment") + raise Exception("Could not refund payment") From f2347c80043ee709618bba5b072e85ecdc23efe0 Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Sun, 17 Dec 2023 23:02:57 +0100 Subject: [PATCH 18/28] made test for vipps callback and checking if order status is successfull --- .../migrations/0058_merge_20231217_2155.py | 13 +++++ app/content/serializers/event.py | 2 +- app/content/util/event_utils.py | 4 +- app/payment/enums.py | 2 +- .../migrations/0003_alter_order_status.py | 30 +++++++++++ app/payment/tests/test_payment_task.py | 34 ++++++++++++- app/payment/urls.py | 2 +- app/payment/util/order_utils.py | 2 +- app/payment/views/order.py | 4 +- app/payment/views/vipps_callback.py | 50 +++++++++++++------ app/tests/payment/test_vipps_callback.py | 44 +++++++++++++--- 11 files changed, 156 insertions(+), 31 deletions(-) create mode 100644 app/content/migrations/0058_merge_20231217_2155.py create mode 100644 app/payment/migrations/0003_alter_order_status.py diff --git a/app/content/migrations/0058_merge_20231217_2155.py b/app/content/migrations/0058_merge_20231217_2155.py new file mode 100644 index 000000000..16c3bfd0f --- /dev/null +++ b/app/content/migrations/0058_merge_20231217_2155.py @@ -0,0 +1,13 @@ +# Generated by Django 4.2.5 on 2023-12-17 20:55 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("content", "0054_registration_payment_expiredate"), + ("content", "0057_event_emojis_allowed_news_emojis_allowed"), + ] + + operations = [] diff --git a/app/content/serializers/event.py b/app/content/serializers/event.py index b7db05569..565638314 100644 --- a/app/content/serializers/event.py +++ b/app/content/serializers/event.py @@ -190,7 +190,7 @@ def update(self, instance, validated_data): if paid_event: paid_event.delete() event.paid_information = None - + if limit_difference > 0 and event.waiting_list_count > 0: event.move_users_from_waiting_list_to_queue(limit_difference) diff --git a/app/content/util/event_utils.py b/app/content/util/event_utils.py index b41f581f7..30852acdf 100644 --- a/app/content/util/event_utils.py +++ b/app/content/util/event_utils.py @@ -4,8 +4,6 @@ from sentry_sdk import capture_exception from app.content.exceptions import RefundFailedError -from app.payment.enums import OrderStatus -from app.payment.models import Order from app.payment.tasks import check_if_has_paid from app.payment.util.payment_utils import ( get_new_access_token, @@ -76,7 +74,7 @@ def refund_vipps_order(order_id, event, transaction_text): os.environ.update({"PAYMENT_ACCESS_TOKEN": access_token}) os.environ.update({"PAYMENT_ACCESS_TOKEN_EXPIRES_AT": str(expires_at)}) - event_price = int(event.paid_information.price * 100) + event_price = int(event.paid_information.price) * 100 try: refund_payment( diff --git a/app/payment/enums.py b/app/payment/enums.py index 21cf044db..c319490b6 100644 --- a/app/payment/enums.py +++ b/app/payment/enums.py @@ -3,7 +3,7 @@ class OrderStatus(TextChoices): INITIATE = "INITIATE" - RESERVE = "RESERVE" + RESERVED = "RESERVED" CAPTURE = "CAPTURE" REFUND = "REFUND" CANCEL = "CANCEL" diff --git a/app/payment/migrations/0003_alter_order_status.py b/app/payment/migrations/0003_alter_order_status.py new file mode 100644 index 000000000..9b8a97f0e --- /dev/null +++ b/app/payment/migrations/0003_alter_order_status.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.5 on 2023-12-17 21:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("payment", "0002_remove_order_expire_date"), + ] + + operations = [ + migrations.AlterField( + model_name="order", + name="status", + field=models.CharField( + choices=[ + ("INITIATE", "Initiate"), + ("RESERVED", "Reserved"), + ("CAPTURE", "Capture"), + ("REFUND", "Refund"), + ("CANCEL", "Cancel"), + ("SALE", "Sale"), + ("VOID", "Void"), + ], + default="INITIATE", + max_length=16, + ), + ), + ] diff --git a/app/payment/tests/test_payment_task.py b/app/payment/tests/test_payment_task.py index 900900a12..3086b2e58 100644 --- a/app/payment/tests/test_payment_task.py +++ b/app/payment/tests/test_payment_task.py @@ -5,6 +5,7 @@ from app.payment.enums import OrderStatus from app.payment.factories import OrderFactory from app.payment.tasks import check_if_has_paid +from app.payment.util.order_utils import check_if_order_is_paid @pytest.fixture() @@ -87,7 +88,7 @@ def test_keep_registration_if_has_reserved_order(event, registration): second_order = OrderFactory(event=event, user=registration.user) third_order = OrderFactory(event=event, user=registration.user) - first_order.status = OrderStatus.RESERVE + first_order.status = OrderStatus.RESERVED first_order.save() second_order.status = OrderStatus.CANCEL second_order.save() @@ -121,3 +122,34 @@ def test_keep_registration_if_has_captured_order(event, registration): registration.refresh_from_db() assert registration + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "order_status", [OrderStatus.CAPTURE, OrderStatus.SALE, OrderStatus.RESERVED] +) +def test_if_order_is_paid(order_status): + """Should return true if order is paid.""" + + order = OrderFactory() + + order.status = order_status + order.save() + + assert check_if_order_is_paid(order) + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "order_status", + [OrderStatus.INITIATE, OrderStatus.VOID, OrderStatus.CANCEL, OrderStatus.REFUND], +) +def test_if_order_is_not_paid(order_status): + """Should return false if order is not paid.""" + + order = OrderFactory() + + order.status = order_status + order.save() + + assert not check_if_order_is_paid(order) diff --git a/app/payment/urls.py b/app/payment/urls.py index 6ba1aa483..cec7c1f92 100644 --- a/app/payment/urls.py +++ b/app/payment/urls.py @@ -10,5 +10,5 @@ urlpatterns = [ re_path(r"", include(router.urls)), - path("v2/payment/", vipps_callback), + path("v2/payments//", vipps_callback), ] diff --git a/app/payment/util/order_utils.py b/app/payment/util/order_utils.py index d03905214..aef59c5f9 100644 --- a/app/payment/util/order_utils.py +++ b/app/payment/util/order_utils.py @@ -15,7 +15,7 @@ def has_paid_order(orders): def check_if_order_is_paid(order): if order and ( order.status == OrderStatus.CAPTURE - or order.status == OrderStatus.RESERVE + or order.status == OrderStatus.RESERVED or order.status == OrderStatus.SALE ): return True diff --git a/app/payment/views/order.py b/app/payment/views/order.py index da82b931b..4ad5a3207 100644 --- a/app/payment/views/order.py +++ b/app/payment/views/order.py @@ -21,9 +21,9 @@ def retrieve(self, request, pk): try: user = request.query_params.get("user_id") event = request.query_params.get("event") - order = Order.objects.filter(user=user, event=event)[0] + orders = Order.objects.filter(user=user, event=event) serializer = OrderSerializer( - order, context={"request": request}, many=False + orders, context={"request": request}, many=True ) return Response(serializer.data, status.HTTP_200_OK) except Order.DoesNotExist as order_not_exist: diff --git a/app/payment/views/vipps_callback.py b/app/payment/views/vipps_callback.py index 060c91ea0..db34988c6 100644 --- a/app/payment/views/vipps_callback.py +++ b/app/payment/views/vipps_callback.py @@ -1,4 +1,7 @@ +import json + from django.conf import settings +from django.http import HttpResponse import requests @@ -6,25 +9,42 @@ from app.payment.util.payment_utils import get_new_access_token -def vipps_callback(_request, order_id): - access_token = get_new_access_token()[1] - url = f"{settings.VIPPS_ORDER_URL}{order_id}/details" - headers = { - "Content-Type": "application/json", - "Ocp-Apim-Subscription-Key": settings.VIPPS_SUBSCRIPTION_KEY, - "Authorization": "Bearer " + access_token, - "Merchant-Serial-Number": settings.VIPPS_MERCHANT_SERIAL_NUMBER, - } - res = requests.get(url, headers=headers) - json = res.json() - status = json["transactionLogHistory"][0]["operation"] +def vipps_callback(request, order_id): + """Callback from vipps.""" order = Order.objects.get(order_id=order_id) - order.status = status - order.save() - return order + body = request.body + data = json.loads(body) + + MSN = data["merchantSerialNumber"] + if MSN != settings.VIPPS_MERCHANT_SERIAL_NUMBER: + return HttpResponse(status=400) + + transaction_info = data["transactionInfo"] + if transaction_info: + new_status = transaction_info["status"] + order.status = new_status + order.save() + + return HttpResponse(status=200) + # access_token = get_new_access_token()[1] + # url = f"{settings.VIPPS_ORDER_URL}{order_id}/details" + # headers = { + # "Content-Type": "application/json", + # "Ocp-Apim-Subscription-Key": settings.VIPPS_SUBSCRIPTION_KEY, + # "Authorization": "Bearer " + access_token, + # "Merchant-Serial-Number": settings.VIPPS_MERCHANT_SERIAL_NUMBER, + # } + # res = requests.get(url, headers=headers) + # json = res.json() + # status = json["transactionLogHistory"][0]["operation"] + # order = Order.objects.get(order_id=order_id) + # order.status = status + # order.save() + # return order def force_payment(order_id): + """Force payment for an order.""" access_token = get_new_access_token()[1] url = f"{settings.VIPPS_FORCE_PAYMENT_URL}{order_id}/approve" headers = { diff --git a/app/tests/payment/test_vipps_callback.py b/app/tests/payment/test_vipps_callback.py index 7a7b7c212..15319568e 100644 --- a/app/tests/payment/test_vipps_callback.py +++ b/app/tests/payment/test_vipps_callback.py @@ -1,12 +1,44 @@ -API_EVENT_BASE_URL = "/events/" +from django.conf import settings +import pytest -def _get_registration_url(event): - return f"{API_EVENT_BASE_URL}{event.pk}/registrations/" +from app.payment.enums import OrderStatus +from app.payment.factories import OrderFactory -def _get_registration_post_data(user, event): +def get_callback_data(order_id, status): return { - "user_id": user.user_id, - "event": event.pk, + "merchantSerialNumber": settings.VIPPS_MERCHANT_SERIAL_NUMBER, + "orderId": order_id, + "transactionInfo": { + "amount": 20000, + "status": status, + "timeStamp": "2018-12-12T11:18:38.246Z", + "transactionId": "5001420062", + }, } + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "status", + [ + OrderStatus.RESERVED, + OrderStatus.CAPTURE, + OrderStatus.REFUND, + OrderStatus.CANCEL, + OrderStatus.SALE, + OrderStatus.VOID, + ], +) +def test_update_order_status_by_vipps_callback(default_client, status): + """Should update order status.""" + + order = OrderFactory(status=OrderStatus.INITIATE) + order_id = order.order_id + + data = get_callback_data(order_id, status) + default_client.post(f"/v2/payments/{order_id}/", data=data) + order.refresh_from_db() + + assert order.status == status From 1928726c15196009742a9600fa709ab3e5af8863 Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Fri, 22 Dec 2023 09:59:12 +0100 Subject: [PATCH 19/28] added check for status when fetching a payment order for a registration on an event --- app/content/exceptions.py | 5 ++ app/content/models/event.py | 5 ++ app/content/models/registration.py | 4 +- app/content/serializers/event.py | 6 +- app/content/serializers/registration.py | 7 +++ app/content/views/event.py | 1 + app/content/views/registration.py | 5 +- app/payment/models/order.py | 4 -- app/payment/util/payment_utils.py | 40 +++++++++++++ app/tests/content/test_event_integration.py | 59 +++++++++++++++++++ .../content/test_registration_integration.py | 35 ----------- app/tests/payment/test_vipps_callback.py | 3 +- 12 files changed, 128 insertions(+), 46 deletions(-) diff --git a/app/content/exceptions.py b/app/content/exceptions.py index 8710a3d7c..c3af02d3b 100644 --- a/app/content/exceptions.py +++ b/app/content/exceptions.py @@ -7,6 +7,11 @@ class APIPaidEventCantBeChangedToFreeEventException(APIException): default_detail = "Arrangementet er et betalt arrangement med påmeldte deltagere, og kan ikke endres til et gratis arrangement" +class APIEventCantBeChangedToPaidEventException(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = "Arrangementet er et gratis arrangement med påmeldte deltagere, og kan ikke endres til et betalt arrangement" + + class APIUserAlreadyAttendedEvent(APIException): status_code = status.HTTP_400_BAD_REQUEST default_detail = "Brukeren har allerede ankommet" diff --git a/app/content/models/event.py b/app/content/models/event.py index ffe9884b7..163e3e10d 100644 --- a/app/content/models/event.py +++ b/app/content/models/event.py @@ -107,6 +107,11 @@ def is_paid_event(self): def list_count(self): """Number of users registered to attend the event""" return self.get_participants().count() + + @property + def has_participants(self): + """Returns if the event has users registered to attend the event""" + return self.list_count > 0 @property def waiting_list_count(self): diff --git a/app/content/models/registration.py b/app/content/models/registration.py index 22cd788b2..09a8ed383 100644 --- a/app/content/models/registration.py +++ b/app/content/models/registration.py @@ -108,6 +108,7 @@ def refund_payment_if_exist(self): event=self.event, transaction_text=f"Refund for {self.event.title} - {self.user.first_name} {self.user.last_name}", ) + self.send_notification_and_mail_for_refund(order) def delete(self, *args, **kwargs): from app.content.util.event_utils import start_payment_countdown @@ -125,7 +126,8 @@ def delete(self, *args, **kwargs): self.delete_submission_if_exists() - self.refund_payment_if_exist() + # TODO: Add this for refund + # self.refund_payment_if_exist() registration = super().delete(*args, **kwargs) if moved_registration: diff --git a/app/content/serializers/event.py b/app/content/serializers/event.py index 565638314..0b4ef5c71 100644 --- a/app/content/serializers/event.py +++ b/app/content/serializers/event.py @@ -5,7 +5,7 @@ from app.common.enums import GroupType from app.common.serializers import BaseModelSerializer -from app.content.exceptions import APIPaidEventCantBeChangedToFreeEventException +from app.content.exceptions import APIPaidEventCantBeChangedToFreeEventException, APIEventCantBeChangedToPaidEventException from app.content.models import Event, PriorityPool from app.content.serializers.priority_pool import ( PriorityPoolCreateSerializer, @@ -183,7 +183,7 @@ def update(self, instance, validated_data): event = super().update(instance, validated_data) if event.is_paid_event: - if not len(paid_information_data) and event.list_count > 0: + if not len(paid_information_data) and event.has_participants: raise APIPaidEventCantBeChangedToFreeEventException() paid_event = PaidEvent.objects.get(event=event) @@ -198,6 +198,8 @@ def update(self, instance, validated_data): event.move_users_from_queue_to_waiting_list(abs(limit_difference)) if paid_information_data and not event.is_paid_event: + if event.has_participants: + raise APIEventCantBeChangedToPaidEventException() PaidEvent.objects.create( event=event, price=paid_information_data["price"], diff --git a/app/content/serializers/registration.py b/app/content/serializers/registration.py index 74b919377..e50ce7326 100644 --- a/app/content/serializers/registration.py +++ b/app/content/serializers/registration.py @@ -10,6 +10,7 @@ from app.forms.enums import EventFormType from app.forms.serializers.submission import SubmissionInRegistrationSerializer from app.payment.util.order_utils import has_paid_order +from app.payment.util.payment_utils import get_payment_order_status class RegistrationSerializer(BaseModelSerializer): @@ -46,6 +47,12 @@ def get_has_unanswered_evaluation(self, obj): def get_has_paid_order(self, obj): orders = obj.event.orders.filter(user=obj.user) + if orders: + order = orders.first() + order_status = get_payment_order_status(order.order_id) + order.status = order_status + order.save() + return has_paid_order(orders) def create(self, validated_data): diff --git a/app/content/views/event.py b/app/content/views/event.py index 55e3f6bcf..ecb522572 100644 --- a/app/content/views/event.py +++ b/app/content/views/event.py @@ -164,6 +164,7 @@ def create(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs): event = Event.objects.get(pk=kwargs["pk"]) if event.is_paid_event: + # TODO: Add refund for all paid orders by participants paid_event = PaidEvent.objects.get(event=kwargs["pk"]) paid_event.delete() diff --git a/app/content/views/registration.py b/app/content/views/registration.py index 51d6fb125..26f5e6377 100644 --- a/app/content/views/registration.py +++ b/app/content/views/registration.py @@ -23,8 +23,8 @@ from app.content.models import Event, Registration, User from app.content.serializers import RegistrationSerializer from app.content.util.event_utils import start_payment_countdown +from app.payment.enums import OrderStatus from app.payment.models.order import Order -from app.payment.views.vipps_callback import vipps_callback class RegistrationViewSet(APIRegistrationErrorsMixin, BaseViewSet): @@ -174,8 +174,7 @@ def add_registration(self, request, *args, **kwargs): user=user, event=event, payment_link=f"https://tihlde.org/arrangementer/{event_id}/", - expire_date=datetime.now(), - status=OrderStatus.SALE, + status=OrderStatus.SALE ) except Exception as e: capture_exception(e) diff --git a/app/payment/models/order.py b/app/payment/models/order.py index 869fb65b8..cbc50bd26 100644 --- a/app/payment/models/order.py +++ b/app/payment/models/order.py @@ -33,7 +33,3 @@ class Meta: def __str__(self): return f"{self.user} - {self.event.title} - {self.status} - {self.created_at}" - - @property - def expired(self): - return now() >= self.expire_date diff --git a/app/payment/util/payment_utils.py b/app/payment/util/payment_utils.py index 8f194d471..e7e2f6118 100644 --- a/app/payment/util/payment_utils.py +++ b/app/payment/util/payment_utils.py @@ -1,4 +1,6 @@ import json +import os +from datetime import datetime from django.conf import settings @@ -27,6 +29,44 @@ def get_new_access_token(): return (response["expires_on"], response["access_token"]) +def check_access_token(): + """ + Checks for access token. + Updates acces token if expired. + Returns new access token. + """ + access_token = os.environ.get("PAYMENT_ACCESS_TOKEN") + expires_at = os.environ.get("PAYMENT_ACCESS_TOKEN_EXPIRES_AT") + + if not access_token or datetime.now() >= datetime.fromtimestamp(int(expires_at)): + (expires_at, access_token) = get_new_access_token() + os.environ.update({"PAYMENT_ACCESS_TOKEN": access_token}) + os.environ.update({"PAYMENT_ACCESS_TOKEN_EXPIRES_AT": str(expires_at)}) + + return access_token + + +def get_payment_order_status(order_id): + """ + Returns status of payment order. + """ + + access_token = check_access_token() + + url = f"{settings.VIPPS_ORDER_URL}{order_id}/details" + headers = { + "Content-Type": "application/json", + "Ocp-Apim-Subscription-Key": settings.VIPPS_SUBSCRIPTION_KEY, + "Authorization": "Bearer " + access_token, + "Merchant-Serial-Number": settings.VIPPS_MERCHANT_SERIAL_NUMBER, + } + + res = requests.get(url, headers=headers) + json = res.json() + + return json["transactionLogHistory"][0]["operation"] + + def initiate_payment(amount, order_id, access_token, transaction_text, fallback): """ Initiate a payment with Vipps diff --git a/app/tests/content/test_event_integration.py b/app/tests/content/test_event_integration.py index 89eae6157..b5a3271eb 100644 --- a/app/tests/content/test_event_integration.py +++ b/app/tests/content/test_event_integration.py @@ -49,6 +49,35 @@ def get_event_data( data["contact_person"] = contact_person return data +def get_paid_event_data( + price, + paytime, + title="New Title", + location="New Location", + organizer=None, + contact_person=None, + limit=0 +): + start_date = timezone.now() + timedelta(days=10) + end_date = timezone.now() + timedelta(days=11) + data = { + "title": title, + "location": location, + "start_date": start_date, + "end_date": end_date, + "is_paid_event": True, + "paid_information": { + "price": price, + "paytime": paytime + }, + "limit": limit, + } + if organizer: + data["organizer"] = organizer + if contact_person: + data["contact_person"] = contact_person + return data + # "event_current_organizer"/"event_new_organizer" should have one of 3 different values: # - None -> The event has no connected organizer/should remove connection to organizer @@ -841,3 +870,33 @@ def test_jubkom_has_create_permission(api_client, jubkom_member): response = client.post(API_EVENTS_BASE_URL, data) assert response.status_code == status.HTTP_201_CREATED + + +@pytest.mark.django_db +def test_update_from_free_event_with_participants_to_paid_event(api_client, admin_user, event, paid_event, registration): + """ + An admin should not be able to update a free event with participants to a paid event. + """ + paid_event.event = event + paid_event.save() + + registration.event = event + registration.is_on_wait = False + registration.save() + + url = f"{API_EVENTS_BASE_URL}{event.id}/" + client = api_client(user=admin_user) + data = get_paid_event_data(price=paid_event.price, paytime="01:00", limit=1) + + repsonse = client.put(url, data) + print(repsonse.data) + + assert repsonse.status_code == status.HTTP_400_BAD_REQUEST + + +@pytest.mark.django_db +def test_update_from_paid_event_with_participants_to_free_event(api_client, admin_user, event, paid_event, registration): + """ + An admin should not be able to update a paid event with participants to a free event. + """ + pass \ No newline at end of file diff --git a/app/tests/content/test_registration_integration.py b/app/tests/content/test_registration_integration.py index 2fb5e89ce..36ddd1c87 100644 --- a/app/tests/content/test_registration_integration.py +++ b/app/tests/content/test_registration_integration.py @@ -1003,41 +1003,6 @@ def test_add_registration_to_event_as_admin_group_member_after_registration_clos assert response.status_code == status.HTTP_201_CREATED -@pytest.mark.django_db -@pytest.mark.parametrize( - "organizer_name", - [ - AdminGroup.PROMO, - AdminGroup.NOK, - AdminGroup.KOK, - AdminGroup.SOSIALEN, - AdminGroup.INDEX, - ], -) -def test_add_registration_to_paid_event_as_admin_group_member( - paid_event, member, organizer_name -): - """ - A member of NOK, Promo, Sosialen or KOK should be able to add a - registration to a paid event manually. A order with status "SALE" should be created. - """ - - data = {"user": member.user_id, "event": paid_event.event.id} - url = f"{_get_registration_url(event=paid_event.event)}add/" - - client = get_api_client(user=member, group_name=organizer_name) - - response = client.post(url, data) - - assert response.status_code == status.HTTP_201_CREATED - - order = Order.objects.filter( - user=member, event=paid_event.event, status=OrderStatus.SALE - ) - - assert order - - @pytest.mark.django_db def test_add_registration_to_event_as_anonymous_user(default_client, event, member): """ diff --git a/app/tests/payment/test_vipps_callback.py b/app/tests/payment/test_vipps_callback.py index 15319568e..a75739041 100644 --- a/app/tests/payment/test_vipps_callback.py +++ b/app/tests/payment/test_vipps_callback.py @@ -38,7 +38,8 @@ def test_update_order_status_by_vipps_callback(default_client, status): order_id = order.order_id data = get_callback_data(order_id, status) - default_client.post(f"/v2/payments/{order_id}/", data=data) + res = default_client.post(f"/v2/payments/{order_id}/", data=data) + print(res.status_code) order.refresh_from_db() assert order.status == status From da2f96c8f8c0e7e307ca49c19da8b74f5edaa345 Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Sat, 23 Dec 2023 18:35:58 +0100 Subject: [PATCH 20/28] made tests for updating and creating paid events --- app/content/models/event.py | 2 +- app/content/models/registration.py | 1 - app/content/serializers/event.py | 52 ++++++---- app/content/views/registration.py | 3 +- app/payment/models/order.py | 1 - app/payment/tasks.py | 2 - app/tests/content/test_event_integration.py | 94 ++++++++++++++++--- .../content/test_registration_integration.py | 2 - 8 files changed, 117 insertions(+), 40 deletions(-) diff --git a/app/content/models/event.py b/app/content/models/event.py index 163e3e10d..26325aa0b 100644 --- a/app/content/models/event.py +++ b/app/content/models/event.py @@ -107,7 +107,7 @@ def is_paid_event(self): def list_count(self): """Number of users registered to attend the event""" return self.get_participants().count() - + @property def has_participants(self): """Returns if the event has users registered to attend the event""" diff --git a/app/content/models/registration.py b/app/content/models/registration.py index 09a8ed383..1ccda13c4 100644 --- a/app/content/models/registration.py +++ b/app/content/models/registration.py @@ -21,7 +21,6 @@ from app.content.models.user import User from app.content.util.registration_utils import get_payment_expiredate from app.forms.enums import EventFormType -from app.payment.models import Order from app.payment.util.order_utils import check_if_order_is_paid, has_paid_order from app.util import now from app.util.models import BaseModel diff --git a/app/content/serializers/event.py b/app/content/serializers/event.py index 0b4ef5c71..be994abbd 100644 --- a/app/content/serializers/event.py +++ b/app/content/serializers/event.py @@ -5,7 +5,10 @@ from app.common.enums import GroupType from app.common.serializers import BaseModelSerializer -from app.content.exceptions import APIPaidEventCantBeChangedToFreeEventException, APIEventCantBeChangedToPaidEventException +from app.content.exceptions import ( + APIEventCantBeChangedToPaidEventException, + APIPaidEventCantBeChangedToFreeEventException, +) from app.content.models import Event, PriorityPool from app.content.serializers.priority_pool import ( PriorityPoolCreateSerializer, @@ -176,20 +179,30 @@ def update(self, instance, validated_data): priority_pools_data = validated_data.pop("priority_pools", None) paid_information_data = validated_data.pop("paid_information", None) limit = validated_data.get("limit") - limit_difference = 0 - if limit: - limit_difference = limit - instance.limit + instance_limit = instance.limit event = super().update(instance, validated_data) - if event.is_paid_event: - if not len(paid_information_data) and event.has_participants: - raise APIPaidEventCantBeChangedToFreeEventException() + self.update_queue(event, limit, instance_limit) - paid_event = PaidEvent.objects.get(event=event) - if paid_event: - paid_event.delete() - event.paid_information = None + self.update_from_free_to_paid(event, paid_information_data) + + self.update_from_paid_to_free(event, paid_information_data) + + if len(paid_information_data): + self.update_paid_information(event, paid_information_data) + + if priority_pools_data: + self.update_priority_pools(event, priority_pools_data) + + event.save() + return event + + def update_queue(self, event, limit, instance_limit): + if not limit: + return + + limit_difference = limit - instance_limit if limit_difference > 0 and event.waiting_list_count > 0: event.move_users_from_waiting_list_to_queue(limit_difference) @@ -197,23 +210,26 @@ def update(self, instance, validated_data): if limit_difference < 0: event.move_users_from_queue_to_waiting_list(abs(limit_difference)) + def update_from_paid_to_free(self, event, paid_information_data): if paid_information_data and not event.is_paid_event: if event.has_participants: raise APIEventCantBeChangedToPaidEventException() + PaidEvent.objects.create( event=event, price=paid_information_data["price"], paytime=paid_information_data["paytime"], ) - if len(paid_information_data): - self.update_paid_information(event, paid_information_data) - - if priority_pools_data: - self.update_priority_pools(event, priority_pools_data) + def update_from_free_to_paid(self, event, paid_information_data): + if event.is_paid_event: + if not len(paid_information_data) and event.has_participants: + raise APIPaidEventCantBeChangedToFreeEventException() - event.save() - return event + paid_event = PaidEvent.objects.filter(event=event) + if paid_event: + paid_event.first().delete() + event.paid_information = None def update_priority_pools(self, event, priority_pools_data): event.priority_pools.all().delete() diff --git a/app/content/views/registration.py b/app/content/views/registration.py index 26f5e6377..11bb2e6ca 100644 --- a/app/content/views/registration.py +++ b/app/content/views/registration.py @@ -1,5 +1,4 @@ import uuid -from datetime import datetime from django.shortcuts import get_object_or_404 from django_filters.rest_framework import DjangoFilterBackend @@ -174,7 +173,7 @@ def add_registration(self, request, *args, **kwargs): user=user, event=event, payment_link=f"https://tihlde.org/arrangementer/{event_id}/", - status=OrderStatus.SALE + status=OrderStatus.SALE, ) except Exception as e: capture_exception(e) diff --git a/app/payment/models/order.py b/app/payment/models/order.py index cbc50bd26..85dbef834 100644 --- a/app/payment/models/order.py +++ b/app/payment/models/order.py @@ -8,7 +8,6 @@ from app.content.models.user import User from app.payment.enums import OrderStatus from app.util.models import BaseModel -from app.util.utils import now class Order(BaseModel, BasePermissionModel): diff --git a/app/payment/tasks.py b/app/payment/tasks.py index 3fe444263..26e13fab9 100644 --- a/app/payment/tasks.py +++ b/app/payment/tasks.py @@ -1,10 +1,8 @@ from app.celery import app from app.content.models.event import Event from app.content.models.registration import Registration -from app.payment.enums import OrderStatus from app.payment.models.order import Order from app.payment.util.order_utils import has_paid_order -from app.payment.views.vipps_callback import vipps_callback from app.util.tasks import BaseTask diff --git a/app/tests/content/test_event_integration.py b/app/tests/content/test_event_integration.py index b5a3271eb..fbf9a6618 100644 --- a/app/tests/content/test_event_integration.py +++ b/app/tests/content/test_event_integration.py @@ -49,6 +49,7 @@ def get_event_data( data["contact_person"] = contact_person return data + def get_paid_event_data( price, paytime, @@ -56,7 +57,7 @@ def get_paid_event_data( location="New Location", organizer=None, contact_person=None, - limit=0 + limit=0, ): start_date = timezone.now() + timedelta(days=10) end_date = timezone.now() + timedelta(days=11) @@ -66,10 +67,7 @@ def get_paid_event_data( "start_date": start_date, "end_date": end_date, "is_paid_event": True, - "paid_information": { - "price": price, - "paytime": paytime - }, + "paid_information": {"price": price, "paytime": paytime}, "limit": limit, } if organizer: @@ -873,10 +871,33 @@ def test_jubkom_has_create_permission(api_client, jubkom_member): @pytest.mark.django_db -def test_update_from_free_event_with_participants_to_paid_event(api_client, admin_user, event, paid_event, registration): +def test_update_from_free_event_with_participants_to_paid_event( + api_client, admin_user, event, registration +): """ An admin should not be able to update a free event with participants to a paid event. """ + + registration.event = event + registration.is_on_wait = False + registration.save() + + url = f"{API_EVENTS_BASE_URL}{event.id}/" + client = api_client(user=admin_user) + data = get_paid_event_data(price=200, paytime="01:00", limit=1) + + response = client.put(url, data) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + +@pytest.mark.django_db +def test_update_from_paid_event_with_participants_to_free_event( + api_client, admin_user, event, paid_event, registration +): + """ + An admin should not be able to update a paid event with participants to a free event. + """ paid_event.event = event paid_event.save() @@ -886,17 +907,64 @@ def test_update_from_free_event_with_participants_to_paid_event(api_client, admi url = f"{API_EVENTS_BASE_URL}{event.id}/" client = api_client(user=admin_user) - data = get_paid_event_data(price=paid_event.price, paytime="01:00", limit=1) + data = get_event_data(limit=1) - repsonse = client.put(url, data) - print(repsonse.data) + response = client.put(url, data) - assert repsonse.status_code == status.HTTP_400_BAD_REQUEST + assert response.status_code == status.HTTP_400_BAD_REQUEST @pytest.mark.django_db -def test_update_from_paid_event_with_participants_to_free_event(api_client, admin_user, event, paid_event, registration): +def test_update_from_paid_event_to_free_event( + api_client, admin_user, event, paid_event +): """ - An admin should not be able to update a paid event with participants to a free event. + An admin should be able to update a paid event with no participants to a free event. + """ + paid_event.event = event + paid_event.save() + + url = f"{API_EVENTS_BASE_URL}{event.id}/" + client = api_client(user=admin_user) + data = get_event_data(limit=0) + + response = client.put(url, data) + + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +def test_update_from_free_event_to_paid_event(api_client, admin_user, event): + """ + An admin should be able to update a free event with no participants to a paid event. + """ + url = f"{API_EVENTS_BASE_URL}{event.id}/" + client = api_client(user=admin_user) + data = get_paid_event_data(price=200, paytime="01:00", limit=1) + + response = client.put(url, data) + + data = response.json() + + assert response.status_code == status.HTTP_200_OK + assert data["is_paid_event"] == True + assert data["paid_information"]["price"] == "200.00" + assert data["paid_information"]["paytime"] == "01:00:00" + + +@pytest.mark.django_db +def test_create_paid_event(api_client, admin_user): """ - pass \ No newline at end of file + An admin should be able to create a paid event. + """ + client = api_client(user=admin_user) + data = get_paid_event_data(price=200, paytime="01:00", limit=1) + + response = client.post(API_EVENTS_BASE_URL, data) + + data = response.json() + + assert response.status_code == status.HTTP_201_CREATED + assert data["is_paid_event"] == True + assert data["paid_information"]["price"] == "200.00" + assert data["paid_information"]["paytime"] == "01:00:00" diff --git a/app/tests/content/test_registration_integration.py b/app/tests/content/test_registration_integration.py index 36ddd1c87..772003d0d 100644 --- a/app/tests/content/test_registration_integration.py +++ b/app/tests/content/test_registration_integration.py @@ -10,8 +10,6 @@ from app.forms.enums import EventFormType from app.forms.tests.form_factories import EventFormFactory, SubmissionFactory from app.group.factories import GroupFactory -from app.payment.enums import OrderStatus -from app.payment.models import Order from app.util.test_utils import add_user_to_group_with_name, get_api_client from app.util.utils import now From c0406224e3ed3410e6dad441aad657c44863e41a Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Sat, 23 Dec 2023 18:38:16 +0100 Subject: [PATCH 21/28] Refactor event integration tests to use boolean truthiness --- app/tests/content/test_event_integration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/tests/content/test_event_integration.py b/app/tests/content/test_event_integration.py index fbf9a6618..bf04f50ef 100644 --- a/app/tests/content/test_event_integration.py +++ b/app/tests/content/test_event_integration.py @@ -947,7 +947,7 @@ def test_update_from_free_event_to_paid_event(api_client, admin_user, event): data = response.json() assert response.status_code == status.HTTP_200_OK - assert data["is_paid_event"] == True + assert data["is_paid_event"] assert data["paid_information"]["price"] == "200.00" assert data["paid_information"]["paytime"] == "01:00:00" @@ -965,6 +965,6 @@ def test_create_paid_event(api_client, admin_user): data = response.json() assert response.status_code == status.HTTP_201_CREATED - assert data["is_paid_event"] == True + assert data["is_paid_event"] assert data["paid_information"]["price"] == "200.00" assert data["paid_information"]["paytime"] == "01:00:00" From e9078235a95f911b72b43d87f7f3345865b3d537 Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Fri, 29 Dec 2023 16:37:56 +0100 Subject: [PATCH 22/28] Refactor payment serializers and views --- app/payment/serializers/__init__.py | 6 +++- app/payment/serializers/order.py | 6 ++++ app/payment/urls.py | 12 ++++---- app/payment/views/vipps.py | 39 ++++++++++++++++++++++++ app/payment/views/vipps_callback.py | 34 --------------------- app/tests/payment/test_vipps_callback.py | 4 +-- 6 files changed, 58 insertions(+), 43 deletions(-) create mode 100644 app/payment/views/vipps.py diff --git a/app/payment/serializers/__init__.py b/app/payment/serializers/__init__.py index 028a0b965..76e890ea0 100644 --- a/app/payment/serializers/__init__.py +++ b/app/payment/serializers/__init__.py @@ -1 +1,5 @@ -from app.payment.serializers.order import OrderSerializer, OrderCreateSerializer +from app.payment.serializers.order import ( + OrderSerializer, + OrderCreateSerializer, + VippsOrderSerialzer, +) diff --git a/app/payment/serializers/order.py b/app/payment/serializers/order.py index c31234948..9e4e9f80d 100644 --- a/app/payment/serializers/order.py +++ b/app/payment/serializers/order.py @@ -11,6 +11,12 @@ class Meta: fields = ("order_id", "status", "payment_link") +class VippsOrderSerialzer(BaseModelSerializer): + class Meta: + model = Order + fields = ("order_id",) + + class OrderCreateSerializer(BaseModelSerializer): class Meta: model = Order diff --git a/app/payment/urls.py b/app/payment/urls.py index cec7c1f92..519b2be49 100644 --- a/app/payment/urls.py +++ b/app/payment/urls.py @@ -1,14 +1,14 @@ -from django.urls import include, path, re_path +from django.urls import include, re_path from rest_framework import routers from app.payment.views.order import OrderViewSet -from app.payment.views.vipps_callback import vipps_callback +from app.payment.views.vipps import VippsViewSet router = routers.DefaultRouter() router.register("payments", OrderViewSet, basename="payment") +router.register( + r"v2/payments/(?P[0-9a-f-]+)", VippsViewSet, basename="payment" +) -urlpatterns = [ - re_path(r"", include(router.urls)), - path("v2/payments//", vipps_callback), -] +urlpatterns = [re_path(r"", include(router.urls))] diff --git a/app/payment/views/vipps.py b/app/payment/views/vipps.py new file mode 100644 index 000000000..570f26877 --- /dev/null +++ b/app/payment/views/vipps.py @@ -0,0 +1,39 @@ +from django.conf import settings +from rest_framework import status +from rest_framework.response import Response + +from app.common.permissions import BasicViewPermission +from app.common.viewsets import BaseViewSet +from app.payment.models import Order +from app.payment.serializers import VippsOrderSerialzer + + +class VippsViewSet(BaseViewSet): + permission_classes = [BasicViewPermission] + serializer_class = VippsOrderSerialzer + queryset = Order.objects.all() + + def create(self, request, order_id): + try: + order = Order.objects.get(order_id=order_id) + data = request.data + + MSN = data.get("merchantSerialNumber") + if int(MSN) != int(settings.VIPPS_MERCHANT_SERIAL_NUMBER): + return Response( + {"detail": "Merchant serial number matcher ikke"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + transaction_info = data.get("transactionInfo") + if transaction_info: + new_status = transaction_info["status"] + order.status = new_status + order.save() + + return Response(status=status.HTTP_200_OK) + except: + return Response( + {"detail": "Kunne ikke oppdatere ordre"}, + status=status.HTTP_500_BAD_REQUEST, + ) diff --git a/app/payment/views/vipps_callback.py b/app/payment/views/vipps_callback.py index db34988c6..b3558086f 100644 --- a/app/payment/views/vipps_callback.py +++ b/app/payment/views/vipps_callback.py @@ -9,40 +9,6 @@ from app.payment.util.payment_utils import get_new_access_token -def vipps_callback(request, order_id): - """Callback from vipps.""" - order = Order.objects.get(order_id=order_id) - body = request.body - data = json.loads(body) - - MSN = data["merchantSerialNumber"] - if MSN != settings.VIPPS_MERCHANT_SERIAL_NUMBER: - return HttpResponse(status=400) - - transaction_info = data["transactionInfo"] - if transaction_info: - new_status = transaction_info["status"] - order.status = new_status - order.save() - - return HttpResponse(status=200) - # access_token = get_new_access_token()[1] - # url = f"{settings.VIPPS_ORDER_URL}{order_id}/details" - # headers = { - # "Content-Type": "application/json", - # "Ocp-Apim-Subscription-Key": settings.VIPPS_SUBSCRIPTION_KEY, - # "Authorization": "Bearer " + access_token, - # "Merchant-Serial-Number": settings.VIPPS_MERCHANT_SERIAL_NUMBER, - # } - # res = requests.get(url, headers=headers) - # json = res.json() - # status = json["transactionLogHistory"][0]["operation"] - # order = Order.objects.get(order_id=order_id) - # order.status = status - # order.save() - # return order - - def force_payment(order_id): """Force payment for an order.""" access_token = get_new_access_token()[1] diff --git a/app/tests/payment/test_vipps_callback.py b/app/tests/payment/test_vipps_callback.py index a75739041..8a4201fa9 100644 --- a/app/tests/payment/test_vipps_callback.py +++ b/app/tests/payment/test_vipps_callback.py @@ -38,8 +38,8 @@ def test_update_order_status_by_vipps_callback(default_client, status): order_id = order.order_id data = get_callback_data(order_id, status) - res = default_client.post(f"/v2/payments/{order_id}/", data=data) - print(res.status_code) + response = default_client.post(f"/v2/payments/{order_id}/", data=data) order.refresh_from_db() + assert response.status_code == 200 assert order.status == status From a67405da950cdaeeda87a3f6cba1ab6f8d329805 Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Fri, 29 Dec 2023 16:55:43 +0100 Subject: [PATCH 23/28] format --- app/payment/views/vipps_callback.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/payment/views/vipps_callback.py b/app/payment/views/vipps_callback.py index b3558086f..754564d79 100644 --- a/app/payment/views/vipps_callback.py +++ b/app/payment/views/vipps_callback.py @@ -1,11 +1,7 @@ -import json - from django.conf import settings -from django.http import HttpResponse import requests -from app.payment.models.order import Order from app.payment.util.payment_utils import get_new_access_token From 1d6f4085ec4614348ed406f73972e87e4a827f20 Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Tue, 2 Jan 2024 11:24:35 +0100 Subject: [PATCH 24/28] format --- app/payment/views/vipps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/payment/views/vipps.py b/app/payment/views/vipps.py index 570f26877..ba7d7ac0c 100644 --- a/app/payment/views/vipps.py +++ b/app/payment/views/vipps.py @@ -32,7 +32,7 @@ def create(self, request, order_id): order.save() return Response(status=status.HTTP_200_OK) - except: + except Exception as e: return Response( {"detail": "Kunne ikke oppdatere ordre"}, status=status.HTTP_500_BAD_REQUEST, From 1652cc8ed7a2a6854b8768b892ed3302bcc8836a Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Tue, 2 Jan 2024 11:26:18 +0100 Subject: [PATCH 25/28] format --- app/payment/views/vipps.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/payment/views/vipps.py b/app/payment/views/vipps.py index ba7d7ac0c..46a685579 100644 --- a/app/payment/views/vipps.py +++ b/app/payment/views/vipps.py @@ -2,6 +2,8 @@ from rest_framework import status from rest_framework.response import Response +from sentry_sdk import capture_exception + from app.common.permissions import BasicViewPermission from app.common.viewsets import BaseViewSet from app.payment.models import Order @@ -33,6 +35,7 @@ def create(self, request, order_id): return Response(status=status.HTTP_200_OK) except Exception as e: + capture_exception(e) return Response( {"detail": "Kunne ikke oppdatere ordre"}, status=status.HTTP_500_BAD_REQUEST, From fc6dc82931a323882cf6306ae91817145b7766c6 Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Tue, 2 Jan 2024 11:37:03 +0100 Subject: [PATCH 26/28] fixed 500 error response and added vipps msn to env in docker --- .github/workflows/ci.yaml | 1 + app/payment/views/vipps.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5de97c5d5..cab568a1b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -41,6 +41,7 @@ jobs: run: | touch .env echo "AZURE_STORAGE_CONNECTION_STRING=${{ secrets.AZURE_STORAGE_CONNECTION_STRING }}" >> .env + echo "VIPPS_MERCHANT_SERIAL_NUMBER=${{ secrets.VIPPS_MERCHANT_SERIAL_NUMBER }}" >> .env - name: Build the Stack run: docker-compose build diff --git a/app/payment/views/vipps.py b/app/payment/views/vipps.py index 46a685579..0cac12686 100644 --- a/app/payment/views/vipps.py +++ b/app/payment/views/vipps.py @@ -38,5 +38,5 @@ def create(self, request, order_id): capture_exception(e) return Response( {"detail": "Kunne ikke oppdatere ordre"}, - status=status.HTTP_500_BAD_REQUEST, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) From 3ffd6a8899a5c7176ae4ba6d5bfa769df9b4058d Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Wed, 3 Jan 2024 16:28:02 +0100 Subject: [PATCH 27/28] Add payment expiration check and handling --- app/content/serializers/registration.py | 4 ++-- app/payment/tests/test_payment_task.py | 29 ++++++++++++++++++++++++- app/payment/util/order_utils.py | 6 +++++ app/payment/views/order.py | 8 +++++++ 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/app/content/serializers/registration.py b/app/content/serializers/registration.py index e50ce7326..724c2c697 100644 --- a/app/content/serializers/registration.py +++ b/app/content/serializers/registration.py @@ -9,6 +9,7 @@ from app.content.util.registration_utils import get_payment_expiredate from app.forms.enums import EventFormType from app.forms.serializers.submission import SubmissionInRegistrationSerializer +from app.payment.enums import OrderStatus from app.payment.util.order_utils import has_paid_order from app.payment.util.payment_utils import get_payment_order_status @@ -47,8 +48,7 @@ def get_has_unanswered_evaluation(self, obj): def get_has_paid_order(self, obj): orders = obj.event.orders.filter(user=obj.user) - if orders: - order = orders.first() + if orders and (order := orders.first()).status == OrderStatus.INITIATE: order_status = get_payment_order_status(order.order_id) order.status = order_status order.save() diff --git a/app/payment/tests/test_payment_task.py b/app/payment/tests/test_payment_task.py index 3086b2e58..a62104c8a 100644 --- a/app/payment/tests/test_payment_task.py +++ b/app/payment/tests/test_payment_task.py @@ -1,3 +1,5 @@ +from django.utils import timezone + import pytest from app.content.factories import EventFactory, RegistrationFactory @@ -5,7 +7,7 @@ from app.payment.enums import OrderStatus from app.payment.factories import OrderFactory from app.payment.tasks import check_if_has_paid -from app.payment.util.order_utils import check_if_order_is_paid +from app.payment.util.order_utils import check_if_order_is_paid, is_expired @pytest.fixture() @@ -153,3 +155,28 @@ def test_if_order_is_not_paid(order_status): order.save() assert not check_if_order_is_paid(order) + + +@pytest.mark.django_db +def test_if_registration_payment_date_is_expired(registration): + """Should return true if registration payment date is expired.""" + + registration.payment_expiredate = timezone.now() - timezone.timedelta(seconds=1) + registration.save() + + assert is_expired(registration.payment_expiredate) + + registration.payment_expiredate = timezone.now() + registration.save() + + assert is_expired(registration.payment_expiredate) + + +@pytest.mark.django_db +def test_if_registration_payment_date_is_not_expired(registration): + """Should return false if registration payment date is not expired.""" + + registration.payment_expiredate = timezone.now() + timezone.timedelta(seconds=1) + registration.save() + + assert not is_expired(registration.payment_expiredate) diff --git a/app/payment/util/order_utils.py b/app/payment/util/order_utils.py index aef59c5f9..8aa4c4b16 100644 --- a/app/payment/util/order_utils.py +++ b/app/payment/util/order_utils.py @@ -1,3 +1,5 @@ +from django.utils import timezone + from app.payment.enums import OrderStatus @@ -21,3 +23,7 @@ def check_if_order_is_paid(order): return True return False + + +def is_expired(expire_date): + return expire_date <= timezone.now() diff --git a/app/payment/views/order.py b/app/payment/views/order.py index 4ad5a3207..139f4afe7 100644 --- a/app/payment/views/order.py +++ b/app/payment/views/order.py @@ -10,6 +10,7 @@ from app.content.models import User from app.payment.models import Order from app.payment.serializers import OrderCreateSerializer, OrderSerializer +from app.payment.util.order_utils import is_expired class OrderViewSet(BaseViewSet, ActionMixin): @@ -36,6 +37,13 @@ def retrieve(self, request, pk): def create(self, request, *args, **kwargs): try: user = get_object_or_404(User, user_id=request.id) + registration = user.registration + + if is_expired(registration.expire_date): + return Response( + {"detail": "Din betalingstid er utgått"}, + status=status.HTTP_400_BAD_REQUEST, + ) serializer = OrderCreateSerializer( data=request.data, From b5c88021ef677e857033c2bd8ad274f4e593ee56 Mon Sep 17 00:00:00 2001 From: Mads Nylund Date: Tue, 9 Jan 2024 19:21:44 +0100 Subject: [PATCH 28/28] fixed bug with expired payment time --- app/payment/views/order.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/payment/views/order.py b/app/payment/views/order.py index 139f4afe7..822a4c978 100644 --- a/app/payment/views/order.py +++ b/app/payment/views/order.py @@ -1,4 +1,3 @@ -from django.shortcuts import get_object_or_404 from rest_framework import status from rest_framework.response import Response @@ -7,7 +6,7 @@ from app.common.mixins import ActionMixin from app.common.permissions import BasicViewPermission from app.common.viewsets import BaseViewSet -from app.content.models import User +from app.content.models import Registration, User from app.payment.models import Order from app.payment.serializers import OrderCreateSerializer, OrderSerializer from app.payment.util.order_utils import is_expired @@ -36,10 +35,11 @@ def retrieve(self, request, pk): def create(self, request, *args, **kwargs): try: - user = get_object_or_404(User, user_id=request.id) - registration = user.registration + user = request.user + event = request.data.get("event") + registration = Registration.objects.get(user=user, event=event) - if is_expired(registration.expire_date): + if is_expired(registration.payment_expiredate): return Response( {"detail": "Din betalingstid er utgått"}, status=status.HTTP_400_BAD_REQUEST,