From 39de92f40562083fccdc537ba3aeeaa811cf769d Mon Sep 17 00:00:00 2001 From: Matti Lamppu Date: Wed, 18 Sep 2024 13:53:43 +0300 Subject: [PATCH] Migrate merchants models to the new app --- .../commands/data_creation/create_caisa.py | 3 +- config/urls.py | 2 +- merchants/admin/__init__.py | 9 - merchants/admin/payment_accounting.py | 42 ---- merchants/admin/payment_merchant.py | 117 ----------- merchants/admin/payment_order.py | 126 ------------ merchants/apps.py | 6 - merchants/enums.py | 63 ------ .../migrations/0008_paymentaccounting.py | 12 +- ...009_alter_payment_accounting_validators.py | 87 +++++++-- .../0017_migrate_nullable_fields.py | 9 +- ...18_remove_verbose_names_and_nullability.py | 27 ++- .../0019_delete_paymentaccounting_and_more.py | 39 ++++ merchants/models/__init__.py | 11 -- merchants/models/payment_accounting.py | 60 ------ merchants/models/payment_merchant.py | 26 --- merchants/models/payment_order.py | 163 ---------------- merchants/models/payment_product.py | 29 --- merchants/pruning.py | 29 --- ...rvationunit_payment_accounting_and_more.py | 52 +++++ reservation_units/models/reservation_unit.py | 9 +- reservation_units/tasks.py | 8 +- .../utils/reservation_unit_payment_helper.py | 2 +- reservations/admin/reservation/admin.py | 4 +- reservations/querysets/reservation.py | 2 +- reservations/tasks.py | 29 ++- ..._alter_unit_payment_accounting_and_more.py | 41 ++++ spaces/models/unit.py | 4 +- tests/factories/order.py | 2 +- tests/factories/payment.py | 6 +- tests/factories/reservation.py | 2 +- .../test_create_order_params.py | 2 +- .../test_verkkokauppa/test_helpers.py | 8 +- .../test_merchant_requests.py | 11 +- .../test_verkkokauppa/test_merchant_types.py | 9 +- .../test_verkkokauppa/test_order_from_json.py | 2 +- .../test_verkkokauppa/test_order_requests.py | 10 +- .../test_verkkokauppa/test_order_types.py | 6 +- .../test_payment_requests.py | 6 +- .../test_verkkokauppa/test_payment_types.py | 4 +- .../test_product_requests.py | 10 +- .../test_verkkokauppa/test_product_types.py | 4 +- .../test_verkkokauppa/test_pruning.py | 26 +-- .../test_refund_from_json.py | 4 +- .../test_refund_status_from_json.py | 4 +- ...ask_refresh_reservation_unit_accounting.py | 6 +- ...efresh_reservation_unit_product_mapping.py | 4 +- .../test_verkkokauppa/test_tasks.py | 2 +- .../test_verkkokauppa/test_validators.py | 2 +- .../test_webhooks/helpers.py | 2 +- .../test_order_cancel_webhooks.py | 8 +- .../test_order_payment_webhooks.py | 6 +- .../test_order_refund_webhooks.py | 6 +- tests/test_graphql_api/test_order/helpers.py | 2 +- .../test_graphql_api/test_order/test_query.py | 2 +- .../test_order/test_refresh.py | 8 +- .../test_order/test_refresh_permissions.py | 2 +- .../test_reservation/test_cancel.py | 4 +- .../test_reservation/test_confirm.py | 8 +- .../test_reservation/test_delete.py | 8 +- .../test_reservation/test_filtering.py | 2 +- .../test_reservation/test_ordering.py | 2 +- .../test_reservation/test_refund.py | 4 +- .../test_update_not_draft.py | 3 +- tests/test_helauth/test_gdpr_api.py | 2 +- ...rune_reservation_with_inactive_payments.py | 2 +- tests/test_utils/test_create_test_data.py | 2 +- tilavarauspalvelu/admin/__init__.py | 6 + .../admin/payment_accounting/admin.py | 42 ++++ .../admin/payment_merchant/admin.py | 117 +++++++++++ .../admin/payment_product/admin.py | 126 ++++++++++++ tilavarauspalvelu/api/graphql/schema.py | 2 +- .../api/graphql/types/merchants/mutations.py | 6 +- .../graphql/types/merchants/permissions.py | 2 +- .../api/graphql/types/merchants/types.py | 4 +- .../graphql/types/reservation/filtersets.py | 2 +- .../graphql/types/reservation/mutations.py | 6 +- .../serializers/cancellation_serializers.py | 4 +- .../serializers/confirm_serializers.py | 12 +- .../serializers/refund_serializers.py | 4 +- .../api/graphql/types/reservation/types.py | 2 +- .../graphql/types/reservation_unit/types.py | 2 +- .../api}/mock_verkkokauppa_api/__init__.py | 0 .../api}/mock_verkkokauppa_api/urls.py | 2 +- .../api}/mock_verkkokauppa_api/views.py | 9 +- tilavarauspalvelu/api/webhooks/views.py | 12 +- tilavarauspalvelu/enums.py | 58 ++++++ .../migrations/0003_merchants.py | 181 +++++++++++++++++ tilavarauspalvelu/models/__init__.py | 8 + .../models/payment_accounting/actions.py | 15 ++ .../models/payment_accounting/model.py | 81 ++++++++ .../models/payment_accounting/queryset.py | 10 + .../models/payment_merchant/actions.py | 15 ++ .../models/payment_merchant/model.py | 46 +++++ .../models/payment_merchant/queryset.py | 10 + .../models/payment_order/actions.py | 15 ++ .../models/payment_order/model.py | 183 ++++++++++++++++++ .../models/payment_order/queryset.py | 10 + .../models/payment_product/actions.py | 15 ++ .../models/payment_product/model.py | 49 +++++ .../models/payment_product/queryset.py | 10 + .../utils}/validators.py | 0 .../utils}/verkkokauppa/__init__.py | 0 .../utils}/verkkokauppa/constants.py | 0 .../utils}/verkkokauppa/exceptions.py | 0 .../utils}/verkkokauppa/helpers.py | 10 +- .../utils}/verkkokauppa/merchants/__init__.py | 0 .../verkkokauppa/merchants/exceptions.py | 2 +- .../utils}/verkkokauppa/merchants/types.py | 4 +- .../utils}/verkkokauppa/order/__init__.py | 0 .../utils}/verkkokauppa/order/exceptions.py | 2 +- .../utils}/verkkokauppa/order/types.py | 4 +- .../utils}/verkkokauppa/payment/__init__.py | 0 .../utils}/verkkokauppa/payment/exceptions.py | 2 +- .../utils}/verkkokauppa/payment/types.py | 10 +- .../utils}/verkkokauppa/product/__init__.py | 0 .../utils}/verkkokauppa/product/exceptions.py | 2 +- .../utils}/verkkokauppa/product/types.py | 2 +- .../verkkokauppa/verkkokauppa_api_client.py | 30 ++- users/anonymisation.py | 2 +- 120 files changed, 1454 insertions(+), 896 deletions(-) delete mode 100644 merchants/admin/__init__.py delete mode 100644 merchants/admin/payment_accounting.py delete mode 100644 merchants/admin/payment_merchant.py delete mode 100644 merchants/admin/payment_order.py delete mode 100644 merchants/apps.py delete mode 100644 merchants/enums.py create mode 100644 merchants/migrations/0019_delete_paymentaccounting_and_more.py delete mode 100644 merchants/models/__init__.py delete mode 100644 merchants/models/payment_accounting.py delete mode 100644 merchants/models/payment_merchant.py delete mode 100644 merchants/models/payment_order.py delete mode 100644 merchants/models/payment_product.py delete mode 100644 merchants/pruning.py create mode 100644 reservation_units/migrations/0107_alter_reservationunit_payment_accounting_and_more.py create mode 100644 spaces/migrations/0040_alter_unit_payment_accounting_and_more.py rename {merchants => tilavarauspalvelu/api}/mock_verkkokauppa_api/__init__.py (100%) rename {merchants => tilavarauspalvelu/api}/mock_verkkokauppa_api/urls.py (83%) rename {merchants => tilavarauspalvelu/api}/mock_verkkokauppa_api/views.py (94%) create mode 100644 tilavarauspalvelu/migrations/0003_merchants.py rename {merchants => tilavarauspalvelu/utils}/validators.py (100%) rename {merchants => tilavarauspalvelu/utils}/verkkokauppa/__init__.py (100%) rename {merchants => tilavarauspalvelu/utils}/verkkokauppa/constants.py (100%) rename {merchants => tilavarauspalvelu/utils}/verkkokauppa/exceptions.py (100%) rename {merchants => tilavarauspalvelu/utils}/verkkokauppa/helpers.py (96%) rename {merchants => tilavarauspalvelu/utils}/verkkokauppa/merchants/__init__.py (100%) rename {merchants => tilavarauspalvelu/utils}/verkkokauppa/merchants/exceptions.py (78%) rename {merchants => tilavarauspalvelu/utils}/verkkokauppa/merchants/types.py (95%) rename {merchants => tilavarauspalvelu/utils}/verkkokauppa/order/__init__.py (100%) rename {merchants => tilavarauspalvelu/utils}/verkkokauppa/order/exceptions.py (74%) rename {merchants => tilavarauspalvelu/utils}/verkkokauppa/order/types.py (98%) rename {merchants => tilavarauspalvelu/utils}/verkkokauppa/payment/__init__.py (100%) rename {merchants => tilavarauspalvelu/utils}/verkkokauppa/payment/exceptions.py (82%) rename {merchants => tilavarauspalvelu/utils}/verkkokauppa/payment/types.py (93%) rename {merchants => tilavarauspalvelu/utils}/verkkokauppa/product/__init__.py (100%) rename {merchants => tilavarauspalvelu/utils}/verkkokauppa/product/exceptions.py (80%) rename {merchants => tilavarauspalvelu/utils}/verkkokauppa/product/types.py (96%) rename {merchants => tilavarauspalvelu/utils}/verkkokauppa/verkkokauppa_api_client.py (93%) diff --git a/common/management/commands/data_creation/create_caisa.py b/common/management/commands/data_creation/create_caisa.py index eb267d9c5..d179eb530 100644 --- a/common/management/commands/data_creation/create_caisa.py +++ b/common/management/commands/data_creation/create_caisa.py @@ -1,6 +1,5 @@ from datetime import date, timedelta -from merchants.models import PaymentAccounting, PaymentMerchant, PaymentProduct from opening_hours.models import OriginHaukiResource from reservation_units.enums import ( AuthenticationType, @@ -24,7 +23,7 @@ from reservations.models import ReservationMetadataSet from spaces.models import Space, Unit from tilavarauspalvelu.enums import TermsOfUseTypeChoices -from tilavarauspalvelu.models import TermsOfUse +from tilavarauspalvelu.models import PaymentAccounting, PaymentMerchant, PaymentProduct, TermsOfUse from .utils import SetName, with_logs diff --git a/config/urls.py b/config/urls.py index 296d04e88..d4fe44e19 100644 --- a/config/urls.py +++ b/config/urls.py @@ -41,7 +41,7 @@ ] if settings.MOCK_VERKKOKAUPPA_API_ENABLED: - urlpatterns.append(path("mock_verkkokauppa/", include("merchants.mock_verkkokauppa_api.urls"))) + urlpatterns.append(path("mock_verkkokauppa/", include("tilavarauspalvelu.api.mock_verkkokauppa_api.urls"))) if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/merchants/admin/__init__.py b/merchants/admin/__init__.py deleted file mode 100644 index 1fef105e8..000000000 --- a/merchants/admin/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from merchants.admin.payment_accounting import PaymentAccountingAdmin -from merchants.admin.payment_merchant import PaymentMerchantAdmin -from merchants.admin.payment_order import PaymentOrderAdmin - -__all__ = [ - "PaymentAccountingAdmin", - "PaymentMerchantAdmin", - "PaymentOrderAdmin", -] diff --git a/merchants/admin/payment_accounting.py b/merchants/admin/payment_accounting.py deleted file mode 100644 index cba1bf87d..000000000 --- a/merchants/admin/payment_accounting.py +++ /dev/null @@ -1,42 +0,0 @@ -from django import forms -from django.contrib import admin - -from merchants.models import PaymentAccounting - -__all__ = [ - "PaymentAccountingAdmin", -] - - -class PaymentAccountingForm(forms.ModelForm): - class Meta: - model = PaymentAccounting - fields = [ - "name", - "company_code", - "main_ledger_account", - "vat_code", - "internal_order", - "profit_center", - "project", - "operation_area", - "balance_profit_center", - ] - # Labels are intentionally left untranslated (TILA-3425) - labels = { - "name": "Accounting name", - "company_code": "Company code", - "main_ledger_account": "Main ledger account", - "vat_code": "VAT code", - "internal_order": "Internal order", - "profit_center": "Profit center", - "project": "Project", - "operation_area": "Operation area", - "balance_profit_center": "Balance profit center", - } - - -@admin.register(PaymentAccounting) -class PaymentAccountingAdmin(admin.ModelAdmin): - # Form - form = PaymentAccountingForm diff --git a/merchants/admin/payment_merchant.py b/merchants/admin/payment_merchant.py deleted file mode 100644 index a5912e2ba..000000000 --- a/merchants/admin/payment_merchant.py +++ /dev/null @@ -1,117 +0,0 @@ -from django import forms -from django.contrib import admin -from django.utils.translation import gettext_lazy as _ - -from merchants.models import PaymentMerchant -from merchants.verkkokauppa.merchants.exceptions import GetMerchantError -from merchants.verkkokauppa.merchants.types import CreateMerchantParams, UpdateMerchantParams -from merchants.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient - -__all__ = [ - "PaymentMerchantAdmin", -] - - -class PaymentMerchantForm(forms.ModelForm): - # paytrail_merchant_id is only used when creating a merchant, later we use the ID returned from the Merchant API - paytrail_merchant_id = forms.CharField( - label="Paytrail merchant ID", # Label is intentionally left untranslated (TILA-3425) - max_length=16, - required=True, - help_text=_("The Paytrail Merchant ID should be a six-digit number."), - ) - - # These fields are saved to / loaded from Merchant API, so they are not part of the model - shop_id = forms.CharField(label=_("Shop ID"), max_length=256, required=True) - business_id = forms.CharField(label=_("Business ID"), max_length=16, required=True) - street = forms.CharField(label=_("Street address"), max_length=128, required=True) - zip = forms.CharField(label=_("ZIP code"), max_length=16, required=True) - city = forms.CharField(label=_("City"), max_length=128, required=True) - email = forms.CharField(label=_("Email address"), max_length=128, required=True) - phone = forms.CharField(label=_("Phone number"), max_length=32, required=True) - url = forms.CharField(label=_("URL"), max_length=256, required=True) - tos_url = forms.CharField(label=_("Terms of service URL"), max_length=256, required=True) - - class Meta: - model = PaymentMerchant - fields = [ - "id", - "name", - "paytrail_merchant_id", - "shop_id", - "business_id", - "street", - "zip", - "city", - "email", - "phone", - "url", - "tos_url", - ] - labels = { - "id": _("Merchant ID"), - "name": _("Merchant name"), - } - help_texts = { - "id": _("Value comes from the Merchant Experience API"), - } - - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - instance: PaymentMerchant | None = kwargs.get("instance", None) - if instance and instance.id: - merchant_info = VerkkokauppaAPIClient.get_merchant(merchant_uuid=instance.id) - if merchant_info is None: - raise GetMerchantError(f"Merchant info for {instance.id!s} not found from Merchant API") - - self.fields["shop_id"].initial = merchant_info.shop_id - self.fields["name"].initial = merchant_info.name - self.fields["street"].initial = merchant_info.street - self.fields["zip"].initial = merchant_info.zip - self.fields["city"].initial = merchant_info.city - self.fields["email"].initial = merchant_info.email - self.fields["phone"].initial = merchant_info.phone - self.fields["url"].initial = merchant_info.url - self.fields["tos_url"].initial = merchant_info.tos_url - self.fields["business_id"].initial = merchant_info.business_id - - # Hide paytrail_merchant_id field when editing an existing merchant - self.fields["paytrail_merchant_id"].required = False - self.fields["paytrail_merchant_id"].widget.input_type = "hidden" - - def save(self, commit=True): - instance: PaymentMerchant | None = self.instance - - if instance: - params = { - "name": self.cleaned_data.get("name", ""), - "street": self.cleaned_data.get("street", ""), - "zip": self.cleaned_data.get("zip", ""), - "city": self.cleaned_data.get("city", ""), - "email": self.cleaned_data.get("email", ""), - "phone": self.cleaned_data.get("phone", ""), - "url": self.cleaned_data.get("url", ""), - "tos_url": self.cleaned_data.get("tos_url", ""), - "business_id": self.cleaned_data.get("business_id", ""), - "shop_id": self.cleaned_data.get("shop_id", ""), - } - - if instance.id is None: - params = CreateMerchantParams( - **params, - paytrail_merchant_id=self.cleaned_data.get("shop_id", ""), - ) - created_merchant = VerkkokauppaAPIClient.create_merchant(params=params) - instance.id = created_merchant.id - else: - params = UpdateMerchantParams(**params) - VerkkokauppaAPIClient.update_merchant(merchant_uuid=instance.id, params=params) - - return super().save(commit=commit) - - -@admin.register(PaymentMerchant) -class PaymentMerchantAdmin(admin.ModelAdmin): - # Form - form = PaymentMerchantForm - readonly_fields = ["id"] diff --git a/merchants/admin/payment_order.py b/merchants/admin/payment_order.py deleted file mode 100644 index b5f88044d..000000000 --- a/merchants/admin/payment_order.py +++ /dev/null @@ -1,126 +0,0 @@ -import uuid -from typing import Any - -from django import forms -from django.contrib import admin -from django.core.handlers.wsgi import WSGIRequest -from django.db import models -from django.utils.translation import gettext_lazy as _ - -from merchants.models import PaymentOrder - -__all__ = [ - "PaymentOrderAdmin", -] - - -class PaymentOrderForm(forms.ModelForm): - class Meta: - model = PaymentOrder - fields = [ - "reservation", - "remote_id", - "payment_id", - "refund_id", - "payment_type", - "status", - "price_net", - "price_vat", - "price_total", - "processed_at", - "language", - "reservation_user_uuid", - "checkout_url", - "receipt_url", - ] - labels = { - "reservation": _("Reservation"), - "remote_id": _("Remote order ID"), - "payment_id": _("Payment ID"), - "refund_id": _("Refund ID"), - "payment_type": _("Payment type"), - "status": _("Payment status"), - "price_net": _("Net amount"), - "price_vat": _("VAT amount"), - "price_total": _("Total amount"), - "processed_at": _("Processed at"), - "language": _("Language"), - "reservation_user_uuid": _("Reservation user UUID"), - "checkout_url": _("Checkout URL"), - "receipt_url": _("Receipt URL"), - } - help_texts = { - "reservation": _("The reservation associated with this payment order"), - "remote_id": _("eCommerce order ID"), - "payment_id": _("eCommerce payment ID"), - "refund_id": _("Available only when order has been refunded"), - } - - def __init__(self, *args, **kwargs) -> None: - """Add reservation and reservation unit to the reservation field help text.""" - super().__init__(*args, **kwargs) - payment_order: PaymentOrder | None = kwargs.get("instance", None) - if payment_order and payment_order.id and payment_order.reservation: - self.fields["reservation"].help_text += ( - "
" + _("Reservation") + f": {payment_order.reservation.id}" - "
" + _("Reservation unit") + f": {payment_order.reservation.reservation_unit.first()}" - ) - - -@admin.register(PaymentOrder) -class PaymentOrderAdmin(admin.ModelAdmin): - # Functions - search_fields = [ - # 'id' handled separately in `get_search_results()` - # 'reservation_id' handled separately in `get_search_results()` - # 'remote_id' handled separately in `get_search_results()` - "reservation__name", - "reservation__reservation_unit__name", - ] - search_help_text = _( - "Search by Payment Order ID, Reservation ID, Remote Order ID, Reservation name, or Reservation Unit name" - ) - - # List - list_display = [ - "id", - "reservation_id", - "remote_id", - "status", - "price_total", - "price_net", - "payment_type", - "processed_at", - "reservation_unit", - ] - list_filter = [ - "status", - "payment_type", - ] - - # Form - form = PaymentOrderForm - - def reservation_unit(self, obj: PaymentOrder) -> str: - return obj.reservation.reservation_unit.first() if obj.reservation else "" - - def get_search_results( - self, - request: WSGIRequest, - queryset: models.QuerySet, - search_term: Any, - ) -> tuple[models.QuerySet, bool]: - queryset, may_have_duplicates = super().get_search_results(request, queryset, search_term) - - if str(search_term).isdigit(): - queryset |= self.model.objects.filter(id__exact=int(search_term)) - queryset |= self.model.objects.filter(reservation__id__exact=int(search_term)) - - try: - term = uuid.UUID(search_term) - except ValueError: - pass - else: - queryset |= self.model.objects.filter(remote_id=term) - - return queryset, may_have_duplicates diff --git a/merchants/apps.py b/merchants/apps.py deleted file mode 100644 index fd690cd69..000000000 --- a/merchants/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class MerchantsConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "merchants" diff --git a/merchants/enums.py b/merchants/enums.py deleted file mode 100644 index 9c42a21fb..000000000 --- a/merchants/enums.py +++ /dev/null @@ -1,63 +0,0 @@ -from __future__ import annotations - -from django.db import models -from django.utils.functional import classproperty -from django.utils.translation import gettext_lazy as _ -from django.utils.translation import pgettext_lazy - -__all__ = [ - "Language", - "OrderStatus", - "OrderStatusWithFree", - "PaymentType", -] - - -class Language(models.TextChoices): - FI = "fi", _("Finnish") - SV = "sv", _("Swedish") - EN = "en", _("English") - - -class OrderStatus(models.TextChoices): - DRAFT = "DRAFT", pgettext_lazy("OrderStatus", "Draft") # Unpaid order - EXPIRED = "EXPIRED", pgettext_lazy("OrderStatus", "Expired") - CANCELLED = "CANCELLED", pgettext_lazy("OrderStatus", "Cancelled") - PAID = "PAID", pgettext_lazy("OrderStatus", "Paid") - PAID_MANUALLY = "PAID_MANUALLY", pgettext_lazy("OrderStatus", "Paid manually") - REFUNDED = "REFUNDED", pgettext_lazy("OrderStatus", "Refunded") - - @classproperty - def needs_update_statuses(cls) -> list[OrderStatus]: - return [ - OrderStatus.DRAFT, - OrderStatus.EXPIRED, - OrderStatus.CANCELLED, - ] - - @classproperty - def can_be_cancelled_statuses(cls) -> list[OrderStatus]: - return [ - OrderStatus.DRAFT, - OrderStatus.EXPIRED, - ] - - -class OrderStatusWithFree(models.TextChoices): - """Same as OrderStatus, but includes the 'FREE' option used for filtering reservations without payments.""" - - # Note: Enums cannot be subclassed, so we have to redefine all "original" members. - DRAFT = "DRAFT", pgettext_lazy("OrderStatus", "Draft") - EXPIRED = "EXPIRED", pgettext_lazy("OrderStatus", "Expired") - CANCELLED = "CANCELLED", pgettext_lazy("OrderStatus", "Cancelled") - PAID = "PAID", pgettext_lazy("OrderStatus", "Paid") - PAID_MANUALLY = "PAID_MANUALLY", pgettext_lazy("OrderStatus", "Paid manually") - REFUNDED = "REFUNDED", pgettext_lazy("OrderStatus", "Refunded") - - FREE = "FREE", pgettext_lazy("OrderStatus", "Free") - - -class PaymentType(models.TextChoices): - ON_SITE = "ON_SITE", pgettext_lazy("PaymentType", "On site") - ONLINE = "ONLINE", pgettext_lazy("PaymentType", "Online") - INVOICE = "INVOICE", pgettext_lazy("PaymentType", "Invoice") diff --git a/merchants/migrations/0008_paymentaccounting.py b/merchants/migrations/0008_paymentaccounting.py index 237512485..2872cbe7b 100644 --- a/merchants/migrations/0008_paymentaccounting.py +++ b/merchants/migrations/0008_paymentaccounting.py @@ -1,8 +1,14 @@ # Generated by Django 3.2.16 on 2022-12-02 07:57 - +from django.core.exceptions import ValidationError from django.db import migrations, models -import merchants.validators + +def validate_accounting_project(project_value: str) -> None: + allowed_lengths = [7, 10, 12, 14, 16] + if len(project_value) not in allowed_lengths: + raise ValidationError( + f"Value must be string of one of the following lenghts: {', '.join(map(str, allowed_lengths))}" + ) class Migration(migrations.Migration): @@ -30,7 +36,7 @@ class Migration(migrations.Migration): blank=True, max_length=16, null=True, - validators=[merchants.validators.validate_accounting_project], + validators=[validate_accounting_project], verbose_name="Project", ), ), diff --git a/merchants/migrations/0009_alter_payment_accounting_validators.py b/merchants/migrations/0009_alter_payment_accounting_validators.py index 7a7917131..2b4f2f341 100644 --- a/merchants/migrations/0009_alter_payment_accounting_validators.py +++ b/merchants/migrations/0009_alter_payment_accounting_validators.py @@ -1,44 +1,89 @@ # Generated by Django 3.2.16 on 2022-12-13 04:38 +import re +from django.core.exceptions import ValidationError from django.db import migrations, models -import merchants.validators -class Migration(migrations.Migration): +def is_numeric(value: str) -> None: + if len(value) > 0 and not re.match(r"^\d*$", value): + raise ValidationError("Value must be numeric") + + +def validate_accounting_project(project_value: str) -> None: + allowed_lengths = [7, 10, 12, 14, 16] + if len(project_value) not in allowed_lengths: + raise ValidationError( + f"Value must be string of one of the following lenghts: {', '.join(map(str, allowed_lengths))}" + ) + +class Migration(migrations.Migration): dependencies = [ - ('merchants', '0008_paymentaccounting'), + ("merchants", "0008_paymentaccounting"), ] operations = [ migrations.AlterField( - model_name='paymentaccounting', - name='company_code', - field=models.CharField(max_length=4, validators=[merchants.validators.is_numeric], verbose_name='Company code'), + model_name="paymentaccounting", + name="company_code", + field=models.CharField( + max_length=4, + validators=[is_numeric], + verbose_name="Company code", + ), ), migrations.AlterField( - model_name='paymentaccounting', - name='internal_order', - field=models.CharField(blank=True, max_length=10, null=True, validators=[merchants.validators.is_numeric], verbose_name='Internal order'), + model_name="paymentaccounting", + name="internal_order", + field=models.CharField( + blank=True, + max_length=10, + null=True, + validators=[is_numeric], + verbose_name="Internal order", + ), ), migrations.AlterField( - model_name='paymentaccounting', - name='main_ledger_account', - field=models.CharField(max_length=6, validators=[merchants.validators.is_numeric], verbose_name='Main ledger account'), + model_name="paymentaccounting", + name="main_ledger_account", + field=models.CharField( + max_length=6, + validators=[is_numeric], + verbose_name="Main ledger account", + ), ), migrations.AlterField( - model_name='paymentaccounting', - name='operation_area', - field=models.CharField(blank=True, max_length=6, null=True, validators=[merchants.validators.is_numeric], verbose_name='Operation area'), + model_name="paymentaccounting", + name="operation_area", + field=models.CharField( + blank=True, + max_length=6, + null=True, + validators=[is_numeric], + verbose_name="Operation area", + ), ), migrations.AlterField( - model_name='paymentaccounting', - name='profit_center', - field=models.CharField(blank=True, max_length=7, null=True, validators=[merchants.validators.is_numeric], verbose_name='Profit center'), + model_name="paymentaccounting", + name="profit_center", + field=models.CharField( + blank=True, + max_length=7, + null=True, + validators=[is_numeric], + verbose_name="Profit center", + ), ), migrations.AlterField( - model_name='paymentaccounting', - name='project', - field=models.CharField(blank=True, max_length=16, null=True, validators=[merchants.validators.validate_accounting_project, merchants.validators.is_numeric], verbose_name='Project'), + model_name="paymentaccounting", + name="project", + field=models.CharField( + blank=True, + max_length=16, + null=True, + validators=[validate_accounting_project, is_numeric], + verbose_name="Project", + ), ), ] diff --git a/merchants/migrations/0017_migrate_nullable_fields.py b/merchants/migrations/0017_migrate_nullable_fields.py index ac92244ff..bea111a54 100644 --- a/merchants/migrations/0017_migrate_nullable_fields.py +++ b/merchants/migrations/0017_migrate_nullable_fields.py @@ -1,18 +1,15 @@ -from typing import TYPE_CHECKING +from __future__ import annotations from django.db import migrations -if TYPE_CHECKING: - from merchants import models as merchants_models - def convert_nullable_fields(apps, schema_editor): - PaymentOrder: merchants_models.PaymentOrder = apps.get_model("merchants", "PaymentOrder") + PaymentOrder = apps.get_model("merchants", "PaymentOrder") PaymentOrder.objects.filter(payment_id=None).update(payment_id="") PaymentOrder.objects.filter(checkout_url=None).update(checkout_url="") PaymentOrder.objects.filter(receipt_url=None).update(receipt_url="") - PaymentAccounting: merchants_models.PaymentAccounting = apps.get_model("merchants", "PaymentAccounting") + PaymentAccounting = apps.get_model("merchants", "PaymentAccounting") PaymentAccounting.objects.filter(internal_order=None).update(internal_order="") PaymentAccounting.objects.filter(profit_center=None).update(profit_center="") PaymentAccounting.objects.filter(project=None).update(project="") diff --git a/merchants/migrations/0018_remove_verbose_names_and_nullability.py b/merchants/migrations/0018_remove_verbose_names_and_nullability.py index 1ca42f302..96ee470f2 100644 --- a/merchants/migrations/0018_remove_verbose_names_and_nullability.py +++ b/merchants/migrations/0018_remove_verbose_names_and_nullability.py @@ -1,9 +1,22 @@ # Generated by Django 5.0.7 on 2024-07-28 19:43 +import re import django.db.models.deletion +from django.core.exceptions import ValidationError from django.db import migrations, models -import merchants.validators + +def is_numeric(value: str) -> None: + if len(value) > 0 and not re.match(r"^\d*$", value): + raise ValidationError("Value must be numeric") + + +def validate_accounting_project(project_value: str) -> None: + allowed_lengths = [7, 10, 12, 14, 16] + if len(project_value) not in allowed_lengths: + raise ValidationError( + f"Value must be string of one of the following lenghts: {', '.join(map(str, allowed_lengths))}" + ) class Migration(migrations.Migration): @@ -21,17 +34,17 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="paymentaccounting", name="company_code", - field=models.CharField(max_length=4, validators=[merchants.validators.is_numeric]), + field=models.CharField(max_length=4, validators=[is_numeric]), ), migrations.AlterField( model_name="paymentaccounting", name="internal_order", - field=models.CharField(blank=True, default="", max_length=10, validators=[merchants.validators.is_numeric]), + field=models.CharField(blank=True, default="", max_length=10, validators=[is_numeric]), ), migrations.AlterField( model_name="paymentaccounting", name="main_ledger_account", - field=models.CharField(max_length=6, validators=[merchants.validators.is_numeric]), + field=models.CharField(max_length=6, validators=[is_numeric]), ), migrations.AlterField( model_name="paymentaccounting", @@ -41,12 +54,12 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="paymentaccounting", name="operation_area", - field=models.CharField(blank=True, default="", max_length=6, validators=[merchants.validators.is_numeric]), + field=models.CharField(blank=True, default="", max_length=6, validators=[is_numeric]), ), migrations.AlterField( model_name="paymentaccounting", name="profit_center", - field=models.CharField(blank=True, default="", max_length=7, validators=[merchants.validators.is_numeric]), + field=models.CharField(blank=True, default="", max_length=7, validators=[is_numeric]), ), migrations.AlterField( model_name="paymentaccounting", @@ -55,7 +68,7 @@ class Migration(migrations.Migration): blank=True, default="", max_length=16, - validators=[merchants.validators.validate_accounting_project, merchants.validators.is_numeric], + validators=[validate_accounting_project, is_numeric], ), ), migrations.AlterField( diff --git a/merchants/migrations/0019_delete_paymentaccounting_and_more.py b/merchants/migrations/0019_delete_paymentaccounting_and_more.py new file mode 100644 index 000000000..e07029f7f --- /dev/null +++ b/merchants/migrations/0019_delete_paymentaccounting_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 5.1.1 on 2024-09-18 10:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("merchants", "0018_remove_verbose_names_and_nullability"), + ("reservation_units", "0107_alter_reservationunit_payment_accounting_and_more"), + ("spaces", "0040_alter_unit_payment_accounting_and_more"), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.DeleteModel( + name="PaymentAccounting", + ), + migrations.RemoveField( + model_name="paymentproduct", + name="merchant", + ), + migrations.RemoveField( + model_name="paymentorder", + name="reservation", + ), + migrations.DeleteModel( + name="PaymentMerchant", + ), + migrations.DeleteModel( + name="PaymentProduct", + ), + migrations.DeleteModel( + name="PaymentOrder", + ), + ], + database_operations=[], + ), + ] diff --git a/merchants/models/__init__.py b/merchants/models/__init__.py deleted file mode 100644 index 8756272b7..000000000 --- a/merchants/models/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from merchants.models.payment_accounting import PaymentAccounting -from merchants.models.payment_merchant import PaymentMerchant -from merchants.models.payment_order import PaymentOrder -from merchants.models.payment_product import PaymentProduct - -__all__ = [ - "PaymentAccounting", - "PaymentMerchant", - "PaymentOrder", - "PaymentProduct", -] diff --git a/merchants/models/payment_accounting.py b/merchants/models/payment_accounting.py deleted file mode 100644 index 8f8d3a00b..000000000 --- a/merchants/models/payment_accounting.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import annotations - -from typing import Any - -from django.conf import settings -from django.core.exceptions import ValidationError -from django.db import models -from django.utils.translation import gettext_lazy as _ - -from merchants.validators import is_numeric, validate_accounting_project - - -class PaymentAccounting(models.Model): - """Custom validation comes from requirements in SAP""" - - name: str = models.CharField(max_length=128) - company_code: str = models.CharField(max_length=4, validators=[is_numeric]) - main_ledger_account: str = models.CharField(max_length=6, validators=[is_numeric]) - vat_code: str = models.CharField(max_length=2) - internal_order: str = models.CharField(blank=True, default="", max_length=10, validators=[is_numeric]) - profit_center: str = models.CharField(blank=True, default="", max_length=7, validators=[is_numeric]) - project: str = models.CharField( - blank=True, - default="", - max_length=16, - validators=[validate_accounting_project, is_numeric], - ) - operation_area: str = models.CharField(blank=True, default="", max_length=6, validators=[is_numeric]) - balance_profit_center: str = models.CharField(max_length=10) - - class Meta: - db_table = "payment_accounting" - base_manager_name = "objects" - ordering = ["pk"] - - def __str__(self) -> str: - return self.name - - def save(self, *args: Any, **kwargs: Any) -> None: - from reservation_units.models import ReservationUnit - from reservation_units.tasks import refresh_reservation_unit_accounting - - super().save(*args, **kwargs) - - if settings.UPDATE_ACCOUNTING: - reservation_units_from_units = ReservationUnit.objects.filter(unit__in=self.units.all()) - reservation_units = reservation_units_from_units.union(self.reservation_units.all()) - for reservation_unit in reservation_units: - refresh_reservation_unit_accounting.delay(reservation_unit.pk) - - def clean(self) -> None: - if not self.project and not self.profit_center and not self.internal_order: - error_message = _("One of the following fields must be given: internal_order, profit_center, project") - raise ValidationError( - { - "internal_order": [error_message], - "profit_center": [error_message], - "project": [error_message], - } - ) diff --git a/merchants/models/payment_merchant.py b/merchants/models/payment_merchant.py deleted file mode 100644 index 1b5e7fc28..000000000 --- a/merchants/models/payment_merchant.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from django.db import models - -if TYPE_CHECKING: - import uuid - - -class PaymentMerchant(models.Model): - """ - ID is not auto-generated. It comes from the Merchant experience API. See admin.py. - https://checkout-test-api.test.hel.ninja/v1/merchant/docs/swagger-ui/ - """ - - id: uuid.UUID = models.UUIDField(primary_key=True) - name: str = models.CharField(max_length=128) - - class Meta: - db_table = "payment_merchant" - base_manager_name = "objects" - ordering = ["pk"] - - def __str__(self) -> str: - return self.name diff --git a/merchants/models/payment_order.py b/merchants/models/payment_order.py deleted file mode 100644 index 690e84426..000000000 --- a/merchants/models/payment_order.py +++ /dev/null @@ -1,163 +0,0 @@ -from __future__ import annotations - -import datetime -from decimal import Decimal -from typing import TYPE_CHECKING, Any - -from django.conf import settings -from django.core.exceptions import ValidationError -from django.db import models -from django.utils.translation import gettext_lazy as _ - -from common.date_utils import local_datetime -from email_notification.helpers.reservation_email_notification_sender import ReservationEmailNotificationSender -from merchants.enums import Language, OrderStatus, PaymentType -from merchants.verkkokauppa.order.exceptions import CancelOrderError -from merchants.verkkokauppa.payment.exceptions import GetPaymentError -from merchants.verkkokauppa.payment.types import Payment -from merchants.verkkokauppa.payment.types import PaymentStatus as WebShopPaymentStatus -from merchants.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient -from reservations.enums import ReservationStateChoice -from utils.sentry import SentryLogger - -if TYPE_CHECKING: - import uuid - - from merchants.verkkokauppa.order.types import Order - from reservations.models import Reservation - - -class PaymentOrder(models.Model): - reservation: Reservation | None = models.ForeignKey( - "reservations.Reservation", - related_name="payment_order", - on_delete=models.SET_NULL, - null=True, - blank=True, - ) - - remote_id: uuid.UUID | None = models.UUIDField(blank=True, null=True) - payment_id: str = models.CharField(blank=True, default="", max_length=128) - refund_id: uuid.UUID | None = models.UUIDField(blank=True, null=True) - payment_type: str = models.CharField(max_length=128, choices=PaymentType.choices) - status: str = models.CharField(max_length=128, choices=OrderStatus.choices, db_index=True) - - price_net: Decimal = models.DecimalField(max_digits=10, decimal_places=2) - price_vat: Decimal = models.DecimalField(max_digits=10, decimal_places=2) - price_total: Decimal = models.DecimalField(max_digits=10, decimal_places=2) - - created_at: datetime.datetime = models.DateTimeField(auto_now_add=True) - processed_at: datetime.datetime | None = models.DateTimeField(null=True, blank=True) - - language: str = models.CharField(max_length=8, choices=Language.choices) - reservation_user_uuid: uuid.UUID | None = models.UUIDField(blank=True, null=True) - checkout_url: str = models.CharField(blank=True, default="", max_length=512) - receipt_url: str = models.CharField(blank=True, default="", max_length=512) - - class Meta: - db_table = "payment_order" - base_manager_name = "objects" - ordering = ["pk"] - - def __str__(self) -> str: - return f"PaymentOrder {self.pk}" - - def save(self, *args: Any, **kwargs: Any) -> PaymentOrder: - self.full_clean() - return super().save(*args, **kwargs) - - def clean(self) -> None: - validation_errors = {} - - failsafe_price_net = self.price_net or Decimal("0.0") - failsafe_price_vat = self.price_vat or Decimal("0.0") - - if self.price_net is not None and self.price_net < Decimal("0.01"): - validation_errors.setdefault("price_net", []).append(_("Must be greater than 0.01")) - if self.price_vat is not None and self.price_vat < Decimal("0"): - validation_errors.setdefault("price_vat", []).append(_("Must be greater than 0")) - if self.price_total is not None and self.price_total != failsafe_price_net + failsafe_price_vat: - validation_errors.setdefault("price_total", []).append(_("Must be the sum of net and vat amounts")) - - if validation_errors: - raise ValidationError(validation_errors) - - @property - def expires_at(self) -> datetime.datetime | None: - if self.status != OrderStatus.DRAFT: - return None - - return self.created_at + datetime.timedelta(minutes=settings.VERKKOKAUPPA_ORDER_EXPIRATION_MINUTES) - - def get_order_payment_from_webshop(self) -> Payment | None: - try: - return VerkkokauppaAPIClient.get_payment(order_uuid=self.remote_id) - except GetPaymentError as err: - SentryLogger.log_exception(err, details="Fetching order payment failed.", remote_id=self.remote_id) - raise - - def cancel_order_in_webshop(self) -> Order | None: - try: - return VerkkokauppaAPIClient.cancel_order(order_uuid=self.remote_id, user_uuid=self.reservation_user_uuid) - except CancelOrderError as err: - SentryLogger.log_exception(err, details="Canceling order failed.", remote_id=self.remote_id) - raise - - def get_order_status_from_webshop_response(self, webshop_payment: Payment | None) -> OrderStatus: - """Determines the order status based on the payment response from the webshop.""" - # Statuses PAID, PAID_MANUALLY and REFUNDED are "final" and should not be updated from the webshop. - if self.status in (OrderStatus.REFUNDED, OrderStatus.PAID, OrderStatus.PAID_MANUALLY): - return OrderStatus(self.status) - - older_than_minutes = settings.VERKKOKAUPPA_ORDER_EXPIRATION_MINUTES - webshop_payment_expires_at = local_datetime() - datetime.timedelta(minutes=older_than_minutes) - - if webshop_payment: - if webshop_payment.status == WebShopPaymentStatus.CANCELLED.value: - return OrderStatus.CANCELLED - if webshop_payment.status == WebShopPaymentStatus.PAID_ONLINE.value: - return OrderStatus.PAID - if ( - webshop_payment.status == WebShopPaymentStatus.CREATED.value - and webshop_payment.timestamp - and webshop_payment.timestamp > webshop_payment_expires_at - ): - # User has entered payment phase in webshop (Payment is created but not yet paid), - # give more time to complete the payment before marking the order as expired. - return OrderStatus.DRAFT - elif not webshop_payment and self.expires_at > local_datetime(): - # User has not entered payment phase in webshop within the expiration time - return OrderStatus.DRAFT - - return OrderStatus.EXPIRED - - def update_order_status(self, new_status: OrderStatus, payment_id: str = "") -> None: - """ - Updates the PaymentOrder status and processed_at timestamp if the status has changed. - - If the order is paid, updates the reservation state to confirmed and sends a confirmation email. - """ - if new_status == self.status: - return - - self.status = new_status - self.processed_at = local_datetime() - if payment_id: - self.payment_id = payment_id - self.save(update_fields=["status", "processed_at", "payment_id"]) - - # If the order is paid, update the reservation state to confirmed and send confirmation email - if ( - self.status == OrderStatus.PAID - and self.reservation is not None - and self.reservation.state == ReservationStateChoice.WAITING_FOR_PAYMENT - ): - self.reservation.state = ReservationStateChoice.CONFIRMED - self.reservation.save(update_fields=["state"]) - ReservationEmailNotificationSender.send_confirmation_email(reservation=self.reservation) - - def refresh_order_status_from_webshop(self): - """Fetches the payment status from the webshop and updates the PaymentOrder status accordingly.""" - webshop_payment: Payment | None = self.get_order_payment_from_webshop() - new_status: OrderStatus = self.get_order_status_from_webshop_response(webshop_payment) - self.update_order_status(new_status, webshop_payment.payment_id if webshop_payment else "") diff --git a/merchants/models/payment_product.py b/merchants/models/payment_product.py deleted file mode 100644 index 035735bbd..000000000 --- a/merchants/models/payment_product.py +++ /dev/null @@ -1,29 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from django.db import models - -if TYPE_CHECKING: - import uuid - - from merchants.models.payment_merchant import PaymentMerchant - - -class PaymentProduct(models.Model): - id: uuid.UUID = models.UUIDField(primary_key=True) - - merchant: PaymentMerchant | None = models.ForeignKey( - "merchants.PaymentMerchant", - related_name="products", - on_delete=models.PROTECT, - null=True, - ) - - class Meta: - db_table = "payment_product" - base_manager_name = "objects" - ordering = ["pk"] - - def __str__(self) -> str: - return str(self.id) diff --git a/merchants/pruning.py b/merchants/pruning.py deleted file mode 100644 index 3287926ad..000000000 --- a/merchants/pruning.py +++ /dev/null @@ -1,29 +0,0 @@ -import contextlib -from datetime import timedelta - -from django.conf import settings -from django.db.transaction import atomic - -from common.date_utils import local_datetime -from merchants.enums import OrderStatus -from merchants.models import PaymentOrder -from merchants.verkkokauppa.order.exceptions import CancelOrderError -from merchants.verkkokauppa.payment.exceptions import GetPaymentError - - -def update_expired_orders() -> None: - older_than_minutes = settings.VERKKOKAUPPA_ORDER_EXPIRATION_MINUTES - expired_datetime = local_datetime() - timedelta(minutes=older_than_minutes) - expired_orders = PaymentOrder.objects.filter( - status=OrderStatus.DRAFT, - created_at__lte=expired_datetime, - remote_id__isnull=False, - ).all() - - for payment_order in expired_orders: - # Do not update the PaymentOrder status if an error occurs - with contextlib.suppress(GetPaymentError, CancelOrderError), atomic(): - payment_order.refresh_order_status_from_webshop() - - if payment_order.status == OrderStatus.EXPIRED: - payment_order.cancel_order_in_webshop() diff --git a/reservation_units/migrations/0107_alter_reservationunit_payment_accounting_and_more.py b/reservation_units/migrations/0107_alter_reservationunit_payment_accounting_and_more.py new file mode 100644 index 000000000..6b22b0630 --- /dev/null +++ b/reservation_units/migrations/0107_alter_reservationunit_payment_accounting_and_more.py @@ -0,0 +1,52 @@ +# Generated by Django 5.1.1 on 2024-09-18 10:41 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("reservation_units", "0106_alter_reservationunit_cancellation_terms_and_more"), + ("tilavarauspalvelu", "0003_merchants"), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.AlterField( + model_name="reservationunit", + name="payment_accounting", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="reservation_units", + to="tilavarauspalvelu.paymentaccounting", + ), + ), + migrations.AlterField( + model_name="reservationunit", + name="payment_merchant", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="reservation_units", + to="tilavarauspalvelu.paymentmerchant", + ), + ), + migrations.AlterField( + model_name="reservationunit", + name="payment_product", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="reservation_units", + to="tilavarauspalvelu.paymentproduct", + ), + ), + ], + database_operations=[], + ), + ] diff --git a/reservation_units/models/reservation_unit.py b/reservation_units/models/reservation_unit.py index ef0edae79..4d3c581af 100644 --- a/reservation_units/models/reservation_unit.py +++ b/reservation_units/models/reservation_unit.py @@ -25,12 +25,11 @@ from reservation_units.querysets import ReservationUnitQuerySet if TYPE_CHECKING: - from merchants.models import PaymentAccounting, PaymentMerchant, PaymentProduct from opening_hours.models import OriginHaukiResource from reservation_units.models import ReservationUnitCancellationRule, ReservationUnitType from reservations.models import ReservationMetadataSet from spaces.models import Unit - from tilavarauspalvelu.models import TermsOfUse + from tilavarauspalvelu.models import PaymentAccounting, PaymentMerchant, PaymentProduct, TermsOfUse __all__ = [ "ReservationUnit", @@ -177,21 +176,21 @@ class ReservationUnit(SearchDocumentMixin, models.Model): on_delete=models.SET_NULL, ) payment_product: PaymentProduct | None = models.ForeignKey( - "merchants.PaymentProduct", + "tilavarauspalvelu.PaymentProduct", related_name="reservation_units", on_delete=models.PROTECT, null=True, blank=True, ) payment_merchant: PaymentMerchant | None = models.ForeignKey( - "merchants.PaymentMerchant", + "tilavarauspalvelu.PaymentMerchant", related_name="reservation_units", on_delete=models.PROTECT, blank=True, null=True, ) payment_accounting: PaymentAccounting | None = models.ForeignKey( - "merchants.PaymentAccounting", + "tilavarauspalvelu.PaymentAccounting", related_name="reservation_units", on_delete=models.PROTECT, null=True, diff --git a/reservation_units/tasks.py b/reservation_units/tasks.py index 6ef5ec5df..5bb9f12d7 100644 --- a/reservation_units/tasks.py +++ b/reservation_units/tasks.py @@ -8,13 +8,13 @@ from easy_thumbnails.exceptions import InvalidImageFormatError from config.celery import app -from merchants.models import PaymentProduct -from merchants.verkkokauppa.product.exceptions import CreateOrUpdateAccountingError -from merchants.verkkokauppa.product.types import CreateOrUpdateAccountingParams, CreateProductParams -from merchants.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from reservation_units.enums import PricingStatus, PricingType from reservation_units.pricing_updates import update_reservation_unit_pricings from reservation_units.utils.reservation_unit_payment_helper import ReservationUnitPaymentHelper +from tilavarauspalvelu.models import PaymentProduct +from tilavarauspalvelu.utils.verkkokauppa.product.exceptions import CreateOrUpdateAccountingError +from tilavarauspalvelu.utils.verkkokauppa.product.types import CreateOrUpdateAccountingParams, CreateProductParams +from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from utils.image_cache import purge from utils.sentry import SentryLogger diff --git a/reservation_units/utils/reservation_unit_payment_helper.py b/reservation_units/utils/reservation_unit_payment_helper.py index 2368786ac..7851043fc 100644 --- a/reservation_units/utils/reservation_unit_payment_helper.py +++ b/reservation_units/utils/reservation_unit_payment_helper.py @@ -1,4 +1,4 @@ -from merchants.models import PaymentAccounting, PaymentMerchant +from tilavarauspalvelu.models import PaymentAccounting, PaymentMerchant class ReservationUnitPaymentHelper: diff --git a/reservations/admin/reservation/admin.py b/reservations/admin/reservation/admin.py index b4064b5c6..e0b67e42e 100644 --- a/reservations/admin/reservation/admin.py +++ b/reservations/admin/reservation/admin.py @@ -14,13 +14,13 @@ from common.date_utils import local_datetime from email_notification.helpers.reservation_email_notification_sender import ReservationEmailNotificationSender -from merchants.enums import OrderStatus -from merchants.models import PaymentOrder from reservations.admin.reservation.filters import PaidReservationListFilter, RecurringReservationListFilter from reservations.admin.reservation.form import ReservationAdminForm from reservations.enums import ReservationStateChoice from reservations.models import Reservation, ReservationDenyReason from reservations.tasks import refund_paid_reservation_task +from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.models import PaymentOrder __all__ = [ "ReservationAdmin", diff --git a/reservations/querysets/reservation.py b/reservations/querysets/reservation.py index a4c029824..6854999e2 100644 --- a/reservations/querysets/reservation.py +++ b/reservations/querysets/reservation.py @@ -9,8 +9,8 @@ from django.db.models.functions import Coalesce from common.date_utils import local_datetime -from merchants.enums import OrderStatus from reservations.enums import ReservationStateChoice +from tilavarauspalvelu.enums import OrderStatus if TYPE_CHECKING: from applications.models import ApplicationRound diff --git a/reservations/tasks.py b/reservations/tasks.py index 0ce36b9fb..1223df07a 100644 --- a/reservations/tasks.py +++ b/reservations/tasks.py @@ -1,14 +1,14 @@ +import datetime import uuid +from contextlib import suppress from django.conf import settings from django.db import transaction from django.db.models import Prefetch +from django.db.transaction import atomic +from common.date_utils import local_datetime from config.celery import app -from merchants.enums import OrderStatus -from merchants.models import PaymentOrder -from merchants.pruning import update_expired_orders -from merchants.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from reservation_units.models import ReservationUnit from reservations.models import ( AffectingTimeSpan, @@ -22,6 +22,11 @@ prune_reservation_statistics, prune_reservation_with_inactive_payments, ) +from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.models import PaymentOrder +from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import CancelOrderError +from tilavarauspalvelu.utils.verkkokauppa.payment.exceptions import GetPaymentError +from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient @app.task(name="prune_reservations") @@ -32,7 +37,21 @@ def prune_reservations_task() -> None: @app.task(name="update_expired_orders") def update_expired_orders_task() -> None: - update_expired_orders() + older_than_minutes = settings.VERKKOKAUPPA_ORDER_EXPIRATION_MINUTES + expired_datetime = local_datetime() - datetime.timedelta(minutes=older_than_minutes) + expired_orders = PaymentOrder.objects.filter( + status=OrderStatus.DRAFT, + created_at__lte=expired_datetime, + remote_id__isnull=False, + ).all() + + for payment_order in expired_orders: + # Do not update the PaymentOrder status if an error occurs + with suppress(GetPaymentError, CancelOrderError), atomic(): + payment_order.refresh_order_status_from_webshop() + + if payment_order.status == OrderStatus.EXPIRED: + payment_order.cancel_order_in_webshop() @app.task(name="prune_reservation_statistics") diff --git a/spaces/migrations/0040_alter_unit_payment_accounting_and_more.py b/spaces/migrations/0040_alter_unit_payment_accounting_and_more.py new file mode 100644 index 000000000..71f0e5763 --- /dev/null +++ b/spaces/migrations/0040_alter_unit_payment_accounting_and_more.py @@ -0,0 +1,41 @@ +# Generated by Django 5.1.1 on 2024-09-18 10:41 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("spaces", "0039_add_tprek_last_modified_field"), + ("tilavarauspalvelu", "0003_merchants"), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.AlterField( + model_name="unit", + name="payment_accounting", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="units", + to="tilavarauspalvelu.paymentaccounting", + ), + ), + migrations.AlterField( + model_name="unit", + name="payment_merchant", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="units", + to="tilavarauspalvelu.paymentmerchant", + ), + ), + ], + database_operations=[], + ), + ] diff --git a/spaces/models/unit.py b/spaces/models/unit.py index 79b3599a7..aec520cb7 100644 --- a/spaces/models/unit.py +++ b/spaces/models/unit.py @@ -38,14 +38,14 @@ class Unit(models.Model): null=True, ) payment_merchant = models.ForeignKey( - "merchants.PaymentMerchant", + "tilavarauspalvelu.PaymentMerchant", related_name="units", on_delete=models.PROTECT, null=True, blank=True, ) payment_accounting = models.ForeignKey( - "merchants.PaymentAccounting", + "tilavarauspalvelu.PaymentAccounting", related_name="units", on_delete=models.PROTECT, null=True, diff --git a/tests/factories/order.py b/tests/factories/order.py index 8c7e41b73..d5891e23b 100644 --- a/tests/factories/order.py +++ b/tests/factories/order.py @@ -5,7 +5,7 @@ import factory from django.utils.timezone import get_default_timezone -from merchants.verkkokauppa.order.types import Order, OrderCustomer +from tilavarauspalvelu.utils.verkkokauppa.order.types import Order, OrderCustomer from ._base import GenericFactory diff --git a/tests/factories/payment.py b/tests/factories/payment.py index 38f69be76..70d85a48e 100644 --- a/tests/factories/payment.py +++ b/tests/factories/payment.py @@ -6,9 +6,9 @@ from django.utils.timezone import get_default_timezone from factory import fuzzy -from merchants.enums import Language, OrderStatus, PaymentType -from merchants.models import PaymentAccounting, PaymentMerchant, PaymentOrder, PaymentProduct -from merchants.verkkokauppa.payment.types import Payment +from tilavarauspalvelu.enums import Language, OrderStatus, PaymentType +from tilavarauspalvelu.models import PaymentAccounting, PaymentMerchant, PaymentOrder, PaymentProduct +from tilavarauspalvelu.utils.verkkokauppa.payment.types import Payment from ._base import GenericDjangoModelFactory, GenericFactory diff --git a/tests/factories/reservation.py b/tests/factories/reservation.py index 6ae11f9ad..171a10fc6 100644 --- a/tests/factories/reservation.py +++ b/tests/factories/reservation.py @@ -5,11 +5,11 @@ from factory import fuzzy from common.date_utils import local_start_of_day, next_hour -from merchants.enums import OrderStatus, PaymentType from reservation_units.enums import PricingType from reservation_units.models import ReservationUnit from reservations.enums import ReservationStateChoice, ReservationTypeChoice from reservations.models import Reservation +from tilavarauspalvelu.enums import OrderStatus, PaymentType from ._base import GenericDjangoModelFactory, ManyToManyFactory, NullableSubFactory, OneToManyFactory diff --git a/tests/test_external_services/test_verkkokauppa/test_create_order_params.py b/tests/test_external_services/test_verkkokauppa/test_create_order_params.py index 460879dbe..61c36c5b5 100644 --- a/tests/test_external_services/test_verkkokauppa/test_create_order_params.py +++ b/tests/test_external_services/test_verkkokauppa/test_create_order_params.py @@ -6,9 +6,9 @@ from django.conf import settings from common.date_utils import local_datetime -from merchants.verkkokauppa.helpers import get_verkkokauppa_order_params from reservations.enums import CustomerTypeChoice from tests.factories import PaymentProductFactory, ReservationFactory, ReservationUnitFactory, UserFactory +from tilavarauspalvelu.utils.verkkokauppa.helpers import get_verkkokauppa_order_params # Applied to all tests pytestmark = [ diff --git a/tests/test_external_services/test_verkkokauppa/test_helpers.py b/tests/test_external_services/test_verkkokauppa/test_helpers.py index 56ca06681..3fd7d68b1 100644 --- a/tests/test_external_services/test_verkkokauppa/test_helpers.py +++ b/tests/test_external_services/test_verkkokauppa/test_helpers.py @@ -4,10 +4,14 @@ from django.utils.timezone import get_default_timezone from freezegun import freeze_time -from merchants.verkkokauppa.exceptions import UnsupportedMetaKeyError -from merchants.verkkokauppa.helpers import get_formatted_reservation_time, get_meta_label, get_verkkokauppa_order_params from reservations.enums import CustomerTypeChoice from tests.factories import PaymentProductFactory, ReservationFactory, ReservationUnitFactory, UserFactory +from tilavarauspalvelu.utils.verkkokauppa.exceptions import UnsupportedMetaKeyError +from tilavarauspalvelu.utils.verkkokauppa.helpers import ( + get_formatted_reservation_time, + get_meta_label, + get_verkkokauppa_order_params, +) # Applied to all tests pytestmark = [ diff --git a/tests/test_external_services/test_verkkokauppa/test_merchant_requests.py b/tests/test_external_services/test_verkkokauppa/test_merchant_requests.py index 7017a724d..5736ef8ae 100644 --- a/tests/test_external_services/test_verkkokauppa/test_merchant_requests.py +++ b/tests/test_external_services/test_verkkokauppa/test_merchant_requests.py @@ -4,11 +4,16 @@ import pytest from django.conf import settings -from merchants.verkkokauppa.merchants.exceptions import CreateMerchantError, UpdateMerchantError -from merchants.verkkokauppa.merchants.types import CreateMerchantParams, Merchant, MerchantInfo, UpdateMerchantParams -from merchants.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from tests.helpers import patch_method from tests.mocks import MockResponse +from tilavarauspalvelu.utils.verkkokauppa.merchants.exceptions import CreateMerchantError, UpdateMerchantError +from tilavarauspalvelu.utils.verkkokauppa.merchants.types import ( + CreateMerchantParams, + Merchant, + MerchantInfo, + UpdateMerchantParams, +) +from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from utils.sentry import SentryLogger create_merchant_params: CreateMerchantParams = CreateMerchantParams( diff --git a/tests/test_external_services/test_verkkokauppa/test_merchant_types.py b/tests/test_external_services/test_verkkokauppa/test_merchant_types.py index 02415417a..f1a9c33da 100644 --- a/tests/test_external_services/test_verkkokauppa/test_merchant_types.py +++ b/tests/test_external_services/test_verkkokauppa/test_merchant_types.py @@ -5,8 +5,13 @@ import pytest from django.conf import settings -from merchants.verkkokauppa.merchants.exceptions import ParseMerchantError -from merchants.verkkokauppa.merchants.types import CreateMerchantParams, Merchant, MerchantInfo, UpdateMerchantParams +from tilavarauspalvelu.utils.verkkokauppa.merchants.exceptions import ParseMerchantError +from tilavarauspalvelu.utils.verkkokauppa.merchants.types import ( + CreateMerchantParams, + Merchant, + MerchantInfo, + UpdateMerchantParams, +) # Applied to all tests pytestmark = [ diff --git a/tests/test_external_services/test_verkkokauppa/test_order_from_json.py b/tests/test_external_services/test_verkkokauppa/test_order_from_json.py index 63b34b01f..05087b078 100644 --- a/tests/test_external_services/test_verkkokauppa/test_order_from_json.py +++ b/tests/test_external_services/test_verkkokauppa/test_order_from_json.py @@ -4,7 +4,7 @@ from django.conf import settings -from merchants.verkkokauppa.order.types import Order +from tilavarauspalvelu.utils.verkkokauppa.order.types import Order order_json = { "orderId": "b6b6b6b6-b6b6-b6b6-b6b6-b6b6b6b6b6b6", diff --git a/tests/test_external_services/test_verkkokauppa/test_order_requests.py b/tests/test_external_services/test_verkkokauppa/test_order_requests.py index 4939e0fc8..8300ff6f5 100644 --- a/tests/test_external_services/test_verkkokauppa/test_order_requests.py +++ b/tests/test_external_services/test_verkkokauppa/test_order_requests.py @@ -7,17 +7,17 @@ import pytest from requests import Timeout -from merchants.verkkokauppa.order.exceptions import CancelOrderError, CreateOrderError, GetOrderError -from merchants.verkkokauppa.order.types import ( +from tests.helpers import patch_method +from tests.mocks import MockResponse +from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import CancelOrderError, CreateOrderError, GetOrderError +from tilavarauspalvelu.utils.verkkokauppa.order.types import ( CreateOrderParams, Order, OrderCustomer, OrderItemMetaParams, OrderItemParams, ) -from merchants.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient -from tests.helpers import patch_method -from tests.mocks import MockResponse +from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from utils.sentry import SentryLogger create_order_params: CreateOrderParams = CreateOrderParams( diff --git a/tests/test_external_services/test_verkkokauppa/test_order_types.py b/tests/test_external_services/test_verkkokauppa/test_order_types.py index 9ff8abe9b..adbca95f3 100644 --- a/tests/test_external_services/test_verkkokauppa/test_order_types.py +++ b/tests/test_external_services/test_verkkokauppa/test_order_types.py @@ -6,8 +6,9 @@ import pytest from django.conf import settings -from merchants.verkkokauppa.order.exceptions import ParseOrderError -from merchants.verkkokauppa.order.types import ( +from tests.helpers import patch_method +from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import ParseOrderError +from tilavarauspalvelu.utils.verkkokauppa.order.types import ( CreateOrderParams, Order, OrderCustomer, @@ -16,7 +17,6 @@ OrderItemMetaParams, OrderItemParams, ) -from tests.helpers import patch_method from utils.sentry import SentryLogger # Applied to all tests diff --git a/tests/test_external_services/test_verkkokauppa/test_payment_requests.py b/tests/test_external_services/test_verkkokauppa/test_payment_requests.py index 980b88623..c2e246ddd 100644 --- a/tests/test_external_services/test_verkkokauppa/test_payment_requests.py +++ b/tests/test_external_services/test_verkkokauppa/test_payment_requests.py @@ -7,11 +7,11 @@ from django.conf import settings from requests import Timeout -from merchants.verkkokauppa.payment.exceptions import GetPaymentError, RefundPaymentError -from merchants.verkkokauppa.payment.types import Payment, Refund, RefundStatusResult -from merchants.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from tests.helpers import patch_method from tests.mocks import MockResponse +from tilavarauspalvelu.utils.verkkokauppa.payment.exceptions import GetPaymentError, RefundPaymentError +from tilavarauspalvelu.utils.verkkokauppa.payment.types import Payment, Refund, RefundStatusResult +from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from utils.sentry import SentryLogger get_payment_response: dict[str, Any] = { diff --git a/tests/test_external_services/test_verkkokauppa/test_payment_types.py b/tests/test_external_services/test_verkkokauppa/test_payment_types.py index 34536f42f..15c00b4b8 100644 --- a/tests/test_external_services/test_verkkokauppa/test_payment_types.py +++ b/tests/test_external_services/test_verkkokauppa/test_payment_types.py @@ -6,9 +6,9 @@ import pytest from django.conf import settings -from merchants.verkkokauppa.payment.exceptions import ParsePaymentError -from merchants.verkkokauppa.payment.types import Payment from tests.helpers import patch_method +from tilavarauspalvelu.utils.verkkokauppa.payment.exceptions import ParsePaymentError +from tilavarauspalvelu.utils.verkkokauppa.payment.types import Payment from utils.sentry import SentryLogger get_payment_response: dict[str, Any] = { diff --git a/tests/test_external_services/test_verkkokauppa/test_product_requests.py b/tests/test_external_services/test_verkkokauppa/test_product_requests.py index ce8feca23..a4f53380c 100644 --- a/tests/test_external_services/test_verkkokauppa/test_product_requests.py +++ b/tests/test_external_services/test_verkkokauppa/test_product_requests.py @@ -4,16 +4,16 @@ from django.conf import settings from requests import Timeout -from merchants.verkkokauppa.product.exceptions import CreateOrUpdateAccountingError, CreateProductError -from merchants.verkkokauppa.product.types import ( +from tests.helpers import patch_method +from tests.mocks import MockResponse +from tilavarauspalvelu.utils.verkkokauppa.product.exceptions import CreateOrUpdateAccountingError, CreateProductError +from tilavarauspalvelu.utils.verkkokauppa.product.types import ( Accounting, CreateOrUpdateAccountingParams, CreateProductParams, Product, ) -from merchants.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient -from tests.helpers import patch_method -from tests.mocks import MockResponse +from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from utils.sentry import SentryLogger create_product_params = CreateProductParams( diff --git a/tests/test_external_services/test_verkkokauppa/test_product_types.py b/tests/test_external_services/test_verkkokauppa/test_product_types.py index 9813b051e..3d9cbdef7 100644 --- a/tests/test_external_services/test_verkkokauppa/test_product_types.py +++ b/tests/test_external_services/test_verkkokauppa/test_product_types.py @@ -2,8 +2,8 @@ import pytest -from merchants.verkkokauppa.product.exceptions import ParseAccountingError, ParseProductError -from merchants.verkkokauppa.product.types import ( +from tilavarauspalvelu.utils.verkkokauppa.product.exceptions import ParseAccountingError, ParseProductError +from tilavarauspalvelu.utils.verkkokauppa.product.types import ( Accounting, CreateOrUpdateAccountingParams, CreateProductParams, diff --git a/tests/test_external_services/test_verkkokauppa/test_pruning.py b/tests/test_external_services/test_verkkokauppa/test_pruning.py index 107a66e67..2768a5e49 100644 --- a/tests/test_external_services/test_verkkokauppa/test_pruning.py +++ b/tests/test_external_services/test_verkkokauppa/test_pruning.py @@ -5,15 +5,15 @@ from common.date_utils import local_datetime from email_notification.helpers.reservation_email_notification_sender import ReservationEmailNotificationSender -from merchants.enums import OrderStatus -from merchants.pruning import update_expired_orders -from merchants.verkkokauppa.order.exceptions import CancelOrderError -from merchants.verkkokauppa.payment.exceptions import GetPaymentError -from merchants.verkkokauppa.payment.types import PaymentStatus -from merchants.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from reservations.enums import ReservationStateChoice +from reservations.tasks import update_expired_orders_task from tests.factories import PaymentFactory, PaymentOrderFactory, ReservationFactory from tests.helpers import patch_method +from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import CancelOrderError +from tilavarauspalvelu.utils.verkkokauppa.payment.exceptions import GetPaymentError +from tilavarauspalvelu.utils.verkkokauppa.payment.types import PaymentStatus +from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from utils.sentry import SentryLogger # Applied to all tests @@ -33,7 +33,7 @@ def test_verkkokauppa_pruning__update_expired_orders__handle_cancelled_orders(or VerkkokauppaAPIClient.get_payment.return_value = PaymentFactory.create(status=PaymentStatus.CANCELLED.value) with freeze_time(order.created_at + timedelta(minutes=6)): - update_expired_orders() + update_expired_orders_task() order.refresh_from_db() assert order.status == OrderStatus.CANCELLED @@ -49,7 +49,7 @@ def test_verkkokauppa_pruning__update_expired_orders__handle_expired_orders(orde ) with freeze_time(order.created_at + timedelta(minutes=6)): - update_expired_orders() + update_expired_orders_task() order.refresh_from_db() assert order.status == OrderStatus.EXPIRED @@ -63,7 +63,7 @@ def test_verkkokauppa_pruning__update_expired_orders__handle_paid_orders(order): VerkkokauppaAPIClient.get_payment.return_value = PaymentFactory.create(status=PaymentStatus.PAID_ONLINE.value) with freeze_time(order.created_at + timedelta(minutes=6)): - update_expired_orders() + update_expired_orders_task() order.refresh_from_db() assert order.status == OrderStatus.PAID @@ -78,7 +78,7 @@ def test_verkkokauppa_pruning__update_expired_orders__handle_paid_orders(order): @patch_method(VerkkokauppaAPIClient.cancel_order) def test_verkkokauppa_pruning__update_expired_orders__handle_missing_payment(order): with freeze_time(order.created_at + timedelta(minutes=6)): - update_expired_orders() + update_expired_orders_task() order.refresh_from_db() assert order.status == OrderStatus.EXPIRED @@ -90,7 +90,7 @@ def test_verkkokauppa_pruning__update_expired_orders__handle_missing_payment(ord @patch_method(VerkkokauppaAPIClient.get_payment, side_effect=GetPaymentError("mock-error")) def test_verkkokauppa_pruning__update_expired_orders__get_payment_errors_are_logged(order): with freeze_time(order.created_at + timedelta(minutes=6)): - update_expired_orders() + update_expired_orders_task() order.refresh_from_db() assert order.status == OrderStatus.DRAFT @@ -108,7 +108,7 @@ def test_verkkokauppa_pruning__update_expired_orders__cancel_error_errors_are_lo ) with freeze_time(order.created_at + timedelta(minutes=6)): - update_expired_orders() + update_expired_orders_task() order.refresh_from_db() assert order.status == OrderStatus.DRAFT @@ -126,7 +126,7 @@ def test_verkkokauppa_pruning__update_expired_orders__give_more_time_if_user_ent ) with freeze_time(order.created_at + timedelta(minutes=6)): - update_expired_orders() + update_expired_orders_task() assert VerkkokauppaAPIClient.get_payment.called is True assert VerkkokauppaAPIClient.cancel_order.called is False diff --git a/tests/test_external_services/test_verkkokauppa/test_refund_from_json.py b/tests/test_external_services/test_verkkokauppa/test_refund_from_json.py index 601ae51eb..bc30b1a91 100644 --- a/tests/test_external_services/test_verkkokauppa/test_refund_from_json.py +++ b/tests/test_external_services/test_verkkokauppa/test_refund_from_json.py @@ -4,9 +4,9 @@ import pytest from django.conf import settings -from merchants.verkkokauppa.payment.exceptions import ParseRefundError -from merchants.verkkokauppa.payment.types import Refund from tests.helpers import patch_method +from tilavarauspalvelu.utils.verkkokauppa.payment.exceptions import ParseRefundError +from tilavarauspalvelu.utils.verkkokauppa.payment.types import Refund from utils.sentry import SentryLogger refund_json = { diff --git a/tests/test_external_services/test_verkkokauppa/test_refund_status_from_json.py b/tests/test_external_services/test_verkkokauppa/test_refund_status_from_json.py index b465f7e1d..9499b4a90 100644 --- a/tests/test_external_services/test_verkkokauppa/test_refund_status_from_json.py +++ b/tests/test_external_services/test_verkkokauppa/test_refund_status_from_json.py @@ -4,9 +4,9 @@ import pytest from django.conf import settings -from merchants.verkkokauppa.payment.exceptions import ParseRefundStatusError -from merchants.verkkokauppa.payment.types import RefundStatusResult from tests.helpers import patch_method +from tilavarauspalvelu.utils.verkkokauppa.payment.exceptions import ParseRefundStatusError +from tilavarauspalvelu.utils.verkkokauppa.payment.types import RefundStatusResult from utils.sentry import SentryLogger refund_status_json = { diff --git a/tests/test_external_services/test_verkkokauppa/test_task_refresh_reservation_unit_accounting.py b/tests/test_external_services/test_verkkokauppa/test_task_refresh_reservation_unit_accounting.py index a5e410a13..ecee8099d 100644 --- a/tests/test_external_services/test_verkkokauppa/test_task_refresh_reservation_unit_accounting.py +++ b/tests/test_external_services/test_verkkokauppa/test_task_refresh_reservation_unit_accounting.py @@ -3,9 +3,6 @@ import pytest from django.test import override_settings -from merchants.verkkokauppa.product.exceptions import CreateOrUpdateAccountingError -from merchants.verkkokauppa.product.types import CreateOrUpdateAccountingParams, Product -from merchants.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from reservation_units.tasks import refresh_reservation_unit_accounting from tests.factories import ( PaymentAccountingFactory, @@ -14,6 +11,9 @@ ReservationUnitPricingFactory, ) from tests.helpers import patch_method +from tilavarauspalvelu.utils.verkkokauppa.product.exceptions import CreateOrUpdateAccountingError +from tilavarauspalvelu.utils.verkkokauppa.product.types import CreateOrUpdateAccountingParams, Product +from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from utils.sentry import SentryLogger # Applied to all tests diff --git a/tests/test_external_services/test_verkkokauppa/test_task_refresh_reservation_unit_product_mapping.py b/tests/test_external_services/test_verkkokauppa/test_task_refresh_reservation_unit_product_mapping.py index 6f7146532..e86e32bd1 100644 --- a/tests/test_external_services/test_verkkokauppa/test_task_refresh_reservation_unit_product_mapping.py +++ b/tests/test_external_services/test_verkkokauppa/test_task_refresh_reservation_unit_product_mapping.py @@ -3,10 +3,10 @@ import pytest from django.test import override_settings -from merchants.verkkokauppa.product.types import Product -from merchants.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from tests.factories import PaymentMerchantFactory, ReservationUnitFactory, ReservationUnitPricingFactory from tests.helpers import patch_method +from tilavarauspalvelu.utils.verkkokauppa.product.types import Product +from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient # Applied to all tests pytestmark = [ diff --git a/tests/test_external_services/test_verkkokauppa/test_tasks.py b/tests/test_external_services/test_verkkokauppa/test_tasks.py index b2ee0650a..7700446db 100644 --- a/tests/test_external_services/test_verkkokauppa/test_tasks.py +++ b/tests/test_external_services/test_verkkokauppa/test_tasks.py @@ -3,10 +3,10 @@ import pytest -from merchants.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from reservations.tasks import refund_paid_reservation_task from tests.factories import PaymentOrderFactory, ReservationFactory from tests.helpers import patch_method +from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient # Applied to all tests pytestmark = [ diff --git a/tests/test_external_services/test_verkkokauppa/test_validators.py b/tests/test_external_services/test_verkkokauppa/test_validators.py index 827d229e0..c18aefbff 100644 --- a/tests/test_external_services/test_verkkokauppa/test_validators.py +++ b/tests/test_external_services/test_verkkokauppa/test_validators.py @@ -1,7 +1,7 @@ import pytest from django.core.exceptions import ValidationError -from merchants.validators import is_numeric, validate_accounting_project +from tilavarauspalvelu.utils.validators import is_numeric, validate_accounting_project def test_is_numeric(): diff --git a/tests/test_external_services/test_verkkokauppa/test_webhooks/helpers.py b/tests/test_external_services/test_verkkokauppa/test_webhooks/helpers.py index 8c9037597..829c7dc65 100644 --- a/tests/test_external_services/test_verkkokauppa/test_webhooks/helpers.py +++ b/tests/test_external_services/test_verkkokauppa/test_webhooks/helpers.py @@ -4,7 +4,7 @@ from django.utils.timezone import get_default_timezone -from merchants.verkkokauppa.payment.types import Payment, PaymentStatus, RefundStatus, RefundStatusResult +from tilavarauspalvelu.utils.verkkokauppa.payment.types import Payment, PaymentStatus, RefundStatus, RefundStatusResult def get_mock_order_payment_api(remote_id: uuid.UUID, payment_id: uuid.UUID, status: str = "") -> Payment: diff --git a/tests/test_external_services/test_verkkokauppa/test_webhooks/test_order_cancel_webhooks.py b/tests/test_external_services/test_verkkokauppa/test_webhooks/test_order_cancel_webhooks.py index ed8e254d1..6d0a15ff7 100644 --- a/tests/test_external_services/test_verkkokauppa/test_webhooks/test_order_cancel_webhooks.py +++ b/tests/test_external_services/test_verkkokauppa/test_webhooks/test_order_cancel_webhooks.py @@ -6,12 +6,12 @@ from django.urls import reverse from django.utils.timezone import get_default_timezone -from merchants.enums import OrderStatus -from merchants.verkkokauppa.order.exceptions import GetOrderError -from merchants.verkkokauppa.order.types import Order -from merchants.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from tests.factories import PaymentOrderFactory from tests.helpers import patch_method +from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import GetOrderError +from tilavarauspalvelu.utils.verkkokauppa.order.types import Order +from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from utils.sentry import SentryLogger # Applied to all tests diff --git a/tests/test_external_services/test_verkkokauppa/test_webhooks/test_order_payment_webhooks.py b/tests/test_external_services/test_verkkokauppa/test_webhooks/test_order_payment_webhooks.py index 5147a7124..496c1e688 100644 --- a/tests/test_external_services/test_verkkokauppa/test_webhooks/test_order_payment_webhooks.py +++ b/tests/test_external_services/test_verkkokauppa/test_webhooks/test_order_payment_webhooks.py @@ -4,13 +4,13 @@ from django.urls import reverse from email_notification.helpers.reservation_email_notification_sender import ReservationEmailNotificationSender -from merchants.enums import OrderStatus -from merchants.verkkokauppa.payment.exceptions import GetPaymentError -from merchants.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from reservations.enums import ReservationStateChoice from tests.factories import PaymentOrderFactory, ReservationFactory from tests.helpers import patch_method from tests.test_external_services.test_verkkokauppa.test_webhooks.helpers import get_mock_order_payment_api +from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.utils.verkkokauppa.payment.exceptions import GetPaymentError +from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from utils.sentry import SentryLogger # Applied to all tests diff --git a/tests/test_external_services/test_verkkokauppa/test_webhooks/test_order_refund_webhooks.py b/tests/test_external_services/test_verkkokauppa/test_webhooks/test_order_refund_webhooks.py index 2c53cbd98..b4b077379 100644 --- a/tests/test_external_services/test_verkkokauppa/test_webhooks/test_order_refund_webhooks.py +++ b/tests/test_external_services/test_verkkokauppa/test_webhooks/test_order_refund_webhooks.py @@ -3,12 +3,12 @@ import pytest from django.urls import reverse -from merchants.enums import OrderStatus -from merchants.verkkokauppa.payment.exceptions import GetRefundStatusError -from merchants.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from tests.factories import PaymentOrderFactory from tests.helpers import patch_method from tests.test_external_services.test_verkkokauppa.test_webhooks.helpers import get_mock_order_refund_api +from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.utils.verkkokauppa.payment.exceptions import GetRefundStatusError +from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from utils.sentry import SentryLogger # Applied to all tests diff --git a/tests/test_graphql_api/test_order/helpers.py b/tests/test_graphql_api/test_order/helpers.py index d0a60c3cf..7a0b67d50 100644 --- a/tests/test_graphql_api/test_order/helpers.py +++ b/tests/test_graphql_api/test_order/helpers.py @@ -3,8 +3,8 @@ from graphene_django_extensions.testing import build_mutation, build_query -from merchants.models import PaymentOrder from tests.factories import PaymentOrderFactory, ReservationFactory, ReservationUnitFactory +from tilavarauspalvelu.models import PaymentOrder from users.models import User order_query = partial( diff --git a/tests/test_graphql_api/test_order/test_query.py b/tests/test_graphql_api/test_order/test_query.py index 31c26451f..6d8abac66 100644 --- a/tests/test_graphql_api/test_order/test_query.py +++ b/tests/test_graphql_api/test_order/test_query.py @@ -1,7 +1,7 @@ import freezegun import pytest -from merchants.enums import OrderStatus +from tilavarauspalvelu.enums import OrderStatus from .helpers import get_order, order_query diff --git a/tests/test_graphql_api/test_order/test_refresh.py b/tests/test_graphql_api/test_order/test_refresh.py index d0dba76ce..9658c78a9 100644 --- a/tests/test_graphql_api/test_order/test_refresh.py +++ b/tests/test_graphql_api/test_order/test_refresh.py @@ -3,13 +3,13 @@ import pytest from email_notification.helpers.reservation_email_notification_sender import ReservationEmailNotificationSender -from merchants.enums import OrderStatus -from merchants.verkkokauppa.payment.exceptions import GetPaymentError -from merchants.verkkokauppa.payment.types import PaymentStatus -from merchants.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from reservations.enums import ReservationStateChoice from tests.factories import PaymentFactory from tests.helpers import patch_method +from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.utils.verkkokauppa.payment.exceptions import GetPaymentError +from tilavarauspalvelu.utils.verkkokauppa.payment.types import PaymentStatus +from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from utils.sentry import SentryLogger from .helpers import REFRESH_MUTATION, get_order diff --git a/tests/test_graphql_api/test_order/test_refresh_permissions.py b/tests/test_graphql_api/test_order/test_refresh_permissions.py index 6d2bc7eae..1d87efc87 100644 --- a/tests/test_graphql_api/test_order/test_refresh_permissions.py +++ b/tests/test_graphql_api/test_order/test_refresh_permissions.py @@ -1,8 +1,8 @@ import pytest -from merchants.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from tests.factories import UserFactory from tests.helpers import patch_method +from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from utils.sentry import SentryLogger from .helpers import REFRESH_MUTATION, get_order diff --git a/tests/test_graphql_api/test_reservation/test_cancel.py b/tests/test_graphql_api/test_reservation/test_cancel.py index df1502be6..b20e93f95 100644 --- a/tests/test_graphql_api/test_reservation/test_cancel.py +++ b/tests/test_graphql_api/test_reservation/test_cancel.py @@ -7,12 +7,12 @@ from common.date_utils import local_datetime from email_notification.models import EmailType -from merchants.enums import OrderStatus, PaymentType -from merchants.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from reservations.enums import ReservationStateChoice from reservations.models import ReservationCancelReason from tests.factories import EmailTemplateFactory, PaymentOrderFactory, ReservationFactory from tests.helpers import patch_method +from tilavarauspalvelu.enums import OrderStatus, PaymentType +from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from .helpers import CANCEL_MUTATION, get_cancel_data diff --git a/tests/test_graphql_api/test_reservation/test_confirm.py b/tests/test_graphql_api/test_reservation/test_confirm.py index adaf91dd8..3b3ffb364 100644 --- a/tests/test_graphql_api/test_reservation/test_confirm.py +++ b/tests/test_graphql_api/test_reservation/test_confirm.py @@ -4,10 +4,6 @@ from graphene_django_extensions.testing import build_mutation from email_notification.models import EmailType -from merchants.enums import OrderStatus, PaymentType -from merchants.models import PaymentOrder -from merchants.verkkokauppa.order.exceptions import CreateOrderError -from merchants.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from reservation_units.enums import PricingType from reservations.enums import ReservationStateChoice from tests.factories import ( @@ -19,6 +15,10 @@ UserFactory, ) from tests.helpers import patch_method +from tilavarauspalvelu.enums import OrderStatus, PaymentType +from tilavarauspalvelu.models import PaymentOrder +from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import CreateOrderError +from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from users.models import ReservationNotification from utils.sentry import SentryLogger diff --git a/tests/test_graphql_api/test_reservation/test_delete.py b/tests/test_graphql_api/test_reservation/test_delete.py index 0e5bf3783..94e413ad8 100644 --- a/tests/test_graphql_api/test_reservation/test_delete.py +++ b/tests/test_graphql_api/test_reservation/test_delete.py @@ -3,14 +3,14 @@ import pytest from email_notification.helpers.reservation_email_notification_sender import ReservationEmailNotificationSender -from merchants.enums import OrderStatus -from merchants.verkkokauppa.order.exceptions import CancelOrderError -from merchants.verkkokauppa.payment.types import PaymentStatus -from merchants.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from reservations.enums import ReservationStateChoice from reservations.models import Reservation from tests.factories import OrderFactory, PaymentFactory, PaymentOrderFactory, ReservationFactory from tests.helpers import patch_method +from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import CancelOrderError +from tilavarauspalvelu.utils.verkkokauppa.payment.types import PaymentStatus +from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from utils.sentry import SentryLogger from .helpers import DELETE_MUTATION, get_delete_data diff --git a/tests/test_graphql_api/test_reservation/test_filtering.py b/tests/test_graphql_api/test_reservation/test_filtering.py index af3fafaad..263a471fe 100644 --- a/tests/test_graphql_api/test_reservation/test_filtering.py +++ b/tests/test_graphql_api/test_reservation/test_filtering.py @@ -5,7 +5,6 @@ from freezegun import freeze_time from common.date_utils import DEFAULT_TIMEZONE -from merchants.enums import OrderStatus from permissions.enums import UserRoleChoice from reservations.enums import ReservationStateChoice, ReservationTypeChoice from tests.factories import ( @@ -19,6 +18,7 @@ UserFactory, ) from tests.test_graphql_api.test_reservation.helpers import reservations_query +from tilavarauspalvelu.enums import OrderStatus # Applied to all tests pytestmark = [ diff --git a/tests/test_graphql_api/test_reservation/test_ordering.py b/tests/test_graphql_api/test_reservation/test_ordering.py index 80ec3486a..df4510211 100644 --- a/tests/test_graphql_api/test_reservation/test_ordering.py +++ b/tests/test_graphql_api/test_reservation/test_ordering.py @@ -3,10 +3,10 @@ import pytest from django.utils import timezone -from merchants.enums import OrderStatus from reservations.enums import CustomerTypeChoice from tests.factories import PaymentOrderFactory, ReservationFactory, ReservationUnitFactory from tests.test_graphql_api.test_reservation.helpers import reservations_query +from tilavarauspalvelu.enums import OrderStatus # Applied to all tests pytestmark = [ diff --git a/tests/test_graphql_api/test_reservation/test_refund.py b/tests/test_graphql_api/test_reservation/test_refund.py index 48bd8bd8e..deca3e6c4 100644 --- a/tests/test_graphql_api/test_reservation/test_refund.py +++ b/tests/test_graphql_api/test_reservation/test_refund.py @@ -6,11 +6,11 @@ import pytest from common.date_utils import local_datetime -from merchants.enums import OrderStatus -from merchants.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from reservations.enums import ReservationStateChoice from tests.factories import ReservationFactory, UserFactory from tests.helpers import patch_method +from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from .helpers import REFUND_MUTATION, get_refund_data diff --git a/tests/test_graphql_api/test_reservation_unit/test_update_not_draft.py b/tests/test_graphql_api/test_reservation_unit/test_update_not_draft.py index 3fbca0275..4441eb1e3 100644 --- a/tests/test_graphql_api/test_reservation_unit/test_update_not_draft.py +++ b/tests/test_graphql_api/test_reservation_unit/test_update_not_draft.py @@ -1,9 +1,8 @@ import pytest -from merchants.enums import PaymentType from reservation_units.enums import ReservationStartInterval from tests.factories import ReservationUnitCancellationRuleFactory, ReservationUnitFactory, TermsOfUseFactory -from tilavarauspalvelu.enums import TermsOfUseTypeChoices +from tilavarauspalvelu.enums import PaymentType, TermsOfUseTypeChoices from .helpers import UPDATE_MUTATION, get_non_draft_update_input_data diff --git a/tests/test_helauth/test_gdpr_api.py b/tests/test_helauth/test_gdpr_api.py index ca2322db1..43f0b4e40 100644 --- a/tests/test_helauth/test_gdpr_api.py +++ b/tests/test_helauth/test_gdpr_api.py @@ -7,10 +7,10 @@ from django.urls import reverse from django.utils import timezone -from merchants.enums import OrderStatus from reservations.enums import ReservationStateChoice from tests.factories import ApplicationFactory, PaymentOrderFactory, ReservationFactory, UserFactory from tests.test_helauth.helpers import get_gdpr_auth_header, patch_oidc_config +from tilavarauspalvelu.enums import OrderStatus if TYPE_CHECKING: from applications.models import Application, ApplicationSection diff --git a/tests/test_tasks/test_prune_reservation_with_inactive_payments.py b/tests/test_tasks/test_prune_reservation_with_inactive_payments.py index f9386f8c2..00c226a29 100644 --- a/tests/test_tasks/test_prune_reservation_with_inactive_payments.py +++ b/tests/test_tasks/test_prune_reservation_with_inactive_payments.py @@ -5,11 +5,11 @@ from freezegun import freeze_time from common.date_utils import local_datetime -from merchants.enums import OrderStatus from reservations.enums import ReservationStateChoice from reservations.models import Reservation from reservations.pruning import prune_reservation_with_inactive_payments from tests.factories import PaymentOrderFactory, ReservationFactory +from tilavarauspalvelu.enums import OrderStatus # Applied to all tests pytestmark = [ diff --git a/tests/test_utils/test_create_test_data.py b/tests/test_utils/test_create_test_data.py index a77ece249..14acf7593 100644 --- a/tests/test_utils/test_create_test_data.py +++ b/tests/test_utils/test_create_test_data.py @@ -5,7 +5,6 @@ from common.management.commands.create_test_data import create_test_data from common.models import RequestLog, SQLLog from email_notification.models import EmailTemplate -from merchants.models import PaymentOrder from reservation_units.models import ( Introduction, Keyword, @@ -26,6 +25,7 @@ ReservationStatisticsReservationUnit, ) from spaces.models import Building, RealEstate +from tilavarauspalvelu.models import PaymentOrder from users.models import PersonalInfoViewLog apps_to_check: list[str] = [ diff --git a/tilavarauspalvelu/admin/__init__.py b/tilavarauspalvelu/admin/__init__.py index 95d630e20..75591113a 100644 --- a/tilavarauspalvelu/admin/__init__.py +++ b/tilavarauspalvelu/admin/__init__.py @@ -1,7 +1,13 @@ +from .payment_accounting.admin import PaymentAccountingAdmin +from .payment_merchant.admin import PaymentMerchantAdmin +from .payment_product.admin import PaymentOrderAdmin from .service.admin import ServiceAdmin from .terms_of_use.admin import TermsOfUseAdmin __all__ = [ + "PaymentAccountingAdmin", + "PaymentMerchantAdmin", + "PaymentOrderAdmin", "ServiceAdmin", "TermsOfUseAdmin", ] diff --git a/tilavarauspalvelu/admin/payment_accounting/admin.py b/tilavarauspalvelu/admin/payment_accounting/admin.py index e69de29bb..068767740 100644 --- a/tilavarauspalvelu/admin/payment_accounting/admin.py +++ b/tilavarauspalvelu/admin/payment_accounting/admin.py @@ -0,0 +1,42 @@ +from django import forms +from django.contrib import admin + +from tilavarauspalvelu.models import PaymentAccounting + +__all__ = [ + "PaymentAccountingAdmin", +] + + +class PaymentAccountingForm(forms.ModelForm): + class Meta: + model = PaymentAccounting + fields = [ + "name", + "company_code", + "main_ledger_account", + "vat_code", + "internal_order", + "profit_center", + "project", + "operation_area", + "balance_profit_center", + ] + # Labels are intentionally left untranslated (TILA-3425) + labels = { + "name": "Accounting name", + "company_code": "Company code", + "main_ledger_account": "Main ledger account", + "vat_code": "VAT code", + "internal_order": "Internal order", + "profit_center": "Profit center", + "project": "Project", + "operation_area": "Operation area", + "balance_profit_center": "Balance profit center", + } + + +@admin.register(PaymentAccounting) +class PaymentAccountingAdmin(admin.ModelAdmin): + # Form + form = PaymentAccountingForm diff --git a/tilavarauspalvelu/admin/payment_merchant/admin.py b/tilavarauspalvelu/admin/payment_merchant/admin.py index e69de29bb..82fba351e 100644 --- a/tilavarauspalvelu/admin/payment_merchant/admin.py +++ b/tilavarauspalvelu/admin/payment_merchant/admin.py @@ -0,0 +1,117 @@ +from django import forms +from django.contrib import admin +from django.utils.translation import gettext_lazy as _ + +from tilavarauspalvelu.models import PaymentMerchant +from tilavarauspalvelu.utils.verkkokauppa.merchants.exceptions import GetMerchantError +from tilavarauspalvelu.utils.verkkokauppa.merchants.types import CreateMerchantParams, UpdateMerchantParams +from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient + +__all__ = [ + "PaymentMerchantAdmin", +] + + +class PaymentMerchantForm(forms.ModelForm): + # paytrail_merchant_id is only used when creating a merchant, later we use the ID returned from the Merchant API + paytrail_merchant_id = forms.CharField( + label="Paytrail merchant ID", # Label is intentionally left untranslated (TILA-3425) + max_length=16, + required=True, + help_text=_("The Paytrail Merchant ID should be a six-digit number."), + ) + + # These fields are saved to / loaded from Merchant API, so they are not part of the model + shop_id = forms.CharField(label=_("Shop ID"), max_length=256, required=True) + business_id = forms.CharField(label=_("Business ID"), max_length=16, required=True) + street = forms.CharField(label=_("Street address"), max_length=128, required=True) + zip = forms.CharField(label=_("ZIP code"), max_length=16, required=True) + city = forms.CharField(label=_("City"), max_length=128, required=True) + email = forms.CharField(label=_("Email address"), max_length=128, required=True) + phone = forms.CharField(label=_("Phone number"), max_length=32, required=True) + url = forms.CharField(label=_("URL"), max_length=256, required=True) + tos_url = forms.CharField(label=_("Terms of service URL"), max_length=256, required=True) + + class Meta: + model = PaymentMerchant + fields = [ + "id", + "name", + "paytrail_merchant_id", + "shop_id", + "business_id", + "street", + "zip", + "city", + "email", + "phone", + "url", + "tos_url", + ] + labels = { + "id": _("Merchant ID"), + "name": _("Merchant name"), + } + help_texts = { + "id": _("Value comes from the Merchant Experience API"), + } + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + instance: PaymentMerchant | None = kwargs.get("instance", None) + if instance and instance.id: + merchant_info = VerkkokauppaAPIClient.get_merchant(merchant_uuid=instance.id) + if merchant_info is None: + raise GetMerchantError(f"Merchant info for {instance.id!s} not found from Merchant API") + + self.fields["shop_id"].initial = merchant_info.shop_id + self.fields["name"].initial = merchant_info.name + self.fields["street"].initial = merchant_info.street + self.fields["zip"].initial = merchant_info.zip + self.fields["city"].initial = merchant_info.city + self.fields["email"].initial = merchant_info.email + self.fields["phone"].initial = merchant_info.phone + self.fields["url"].initial = merchant_info.url + self.fields["tos_url"].initial = merchant_info.tos_url + self.fields["business_id"].initial = merchant_info.business_id + + # Hide paytrail_merchant_id field when editing an existing merchant + self.fields["paytrail_merchant_id"].required = False + self.fields["paytrail_merchant_id"].widget.input_type = "hidden" + + def save(self, commit=True): + instance: PaymentMerchant | None = self.instance + + if instance: + params = { + "name": self.cleaned_data.get("name", ""), + "street": self.cleaned_data.get("street", ""), + "zip": self.cleaned_data.get("zip", ""), + "city": self.cleaned_data.get("city", ""), + "email": self.cleaned_data.get("email", ""), + "phone": self.cleaned_data.get("phone", ""), + "url": self.cleaned_data.get("url", ""), + "tos_url": self.cleaned_data.get("tos_url", ""), + "business_id": self.cleaned_data.get("business_id", ""), + "shop_id": self.cleaned_data.get("shop_id", ""), + } + + if instance.id is None: + params = CreateMerchantParams( + **params, + paytrail_merchant_id=self.cleaned_data.get("shop_id", ""), + ) + created_merchant = VerkkokauppaAPIClient.create_merchant(params=params) + instance.id = created_merchant.id + else: + params = UpdateMerchantParams(**params) + VerkkokauppaAPIClient.update_merchant(merchant_uuid=instance.id, params=params) + + return super().save(commit=commit) + + +@admin.register(PaymentMerchant) +class PaymentMerchantAdmin(admin.ModelAdmin): + # Form + form = PaymentMerchantForm + readonly_fields = ["id"] diff --git a/tilavarauspalvelu/admin/payment_product/admin.py b/tilavarauspalvelu/admin/payment_product/admin.py index e69de29bb..eefc9f733 100644 --- a/tilavarauspalvelu/admin/payment_product/admin.py +++ b/tilavarauspalvelu/admin/payment_product/admin.py @@ -0,0 +1,126 @@ +import uuid +from typing import Any + +from django import forms +from django.contrib import admin +from django.core.handlers.wsgi import WSGIRequest +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from tilavarauspalvelu.models import PaymentOrder + +__all__ = [ + "PaymentOrderAdmin", +] + + +class PaymentOrderForm(forms.ModelForm): + class Meta: + model = PaymentOrder + fields = [ + "reservation", + "remote_id", + "payment_id", + "refund_id", + "payment_type", + "status", + "price_net", + "price_vat", + "price_total", + "processed_at", + "language", + "reservation_user_uuid", + "checkout_url", + "receipt_url", + ] + labels = { + "reservation": _("Reservation"), + "remote_id": _("Remote order ID"), + "payment_id": _("Payment ID"), + "refund_id": _("Refund ID"), + "payment_type": _("Payment type"), + "status": _("Payment status"), + "price_net": _("Net amount"), + "price_vat": _("VAT amount"), + "price_total": _("Total amount"), + "processed_at": _("Processed at"), + "language": _("Language"), + "reservation_user_uuid": _("Reservation user UUID"), + "checkout_url": _("Checkout URL"), + "receipt_url": _("Receipt URL"), + } + help_texts = { + "reservation": _("The reservation associated with this payment order"), + "remote_id": _("eCommerce order ID"), + "payment_id": _("eCommerce payment ID"), + "refund_id": _("Available only when order has been refunded"), + } + + def __init__(self, *args, **kwargs) -> None: + """Add reservation and reservation unit to the reservation field help text.""" + super().__init__(*args, **kwargs) + payment_order: PaymentOrder | None = kwargs.get("instance", None) + if payment_order and payment_order.id and payment_order.reservation: + self.fields["reservation"].help_text += ( + "
" + _("Reservation") + f": {payment_order.reservation.id}" + "
" + _("Reservation unit") + f": {payment_order.reservation.reservation_unit.first()}" + ) + + +@admin.register(PaymentOrder) +class PaymentOrderAdmin(admin.ModelAdmin): + # Functions + search_fields = [ + # 'id' handled separately in `get_search_results()` + # 'reservation_id' handled separately in `get_search_results()` + # 'remote_id' handled separately in `get_search_results()` + "reservation__name", + "reservation__reservation_unit__name", + ] + search_help_text = _( + "Search by Payment Order ID, Reservation ID, Remote Order ID, Reservation name, or Reservation Unit name" + ) + + # List + list_display = [ + "id", + "reservation_id", + "remote_id", + "status", + "price_total", + "price_net", + "payment_type", + "processed_at", + "reservation_unit", + ] + list_filter = [ + "status", + "payment_type", + ] + + # Form + form = PaymentOrderForm + + def reservation_unit(self, obj: PaymentOrder) -> str: + return obj.reservation.reservation_unit.first() if obj.reservation else "" + + def get_search_results( + self, + request: WSGIRequest, + queryset: models.QuerySet, + search_term: Any, + ) -> tuple[models.QuerySet, bool]: + queryset, may_have_duplicates = super().get_search_results(request, queryset, search_term) + + if str(search_term).isdigit(): + queryset |= self.model.objects.filter(id__exact=int(search_term)) + queryset |= self.model.objects.filter(reservation__id__exact=int(search_term)) + + try: + term = uuid.UUID(search_term) + except ValueError: + pass + else: + queryset |= self.model.objects.filter(remote_id=term) + + return queryset, may_have_duplicates diff --git a/tilavarauspalvelu/api/graphql/schema.py b/tilavarauspalvelu/api/graphql/schema.py index a7d646ae6..fa660e1f3 100644 --- a/tilavarauspalvelu/api/graphql/schema.py +++ b/tilavarauspalvelu/api/graphql/schema.py @@ -14,9 +14,9 @@ from applications.models import AllocatedTimeSlot from common.models import BannerNotification from common.typing import AnyUser, GQLInfo -from merchants.models import PaymentOrder from permissions.enums import UserPermissionChoice from reservations.models import Reservation +from tilavarauspalvelu.models import PaymentOrder from users.helauth.clients import HelsinkiProfileClient from users.helauth.typing import UserProfileInfo from users.models import User diff --git a/tilavarauspalvelu/api/graphql/types/merchants/mutations.py b/tilavarauspalvelu/api/graphql/types/merchants/mutations.py index 4802ce865..0e35b5dbd 100644 --- a/tilavarauspalvelu/api/graphql/types/merchants/mutations.py +++ b/tilavarauspalvelu/api/graphql/types/merchants/mutations.py @@ -4,10 +4,10 @@ from graphene_django_extensions.bases import DjangoMutation from common.typing import GQLInfo -from merchants.enums import OrderStatus -from merchants.models import PaymentOrder -from merchants.verkkokauppa.payment.exceptions import GetPaymentError from tilavarauspalvelu.api.graphql.extensions.validation_errors import ValidationErrorCodes, ValidationErrorWithCode +from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.models import PaymentOrder +from tilavarauspalvelu.utils.verkkokauppa.payment.exceptions import GetPaymentError from utils.sentry import SentryLogger from .permissions import OrderRefreshPermission diff --git a/tilavarauspalvelu/api/graphql/types/merchants/permissions.py b/tilavarauspalvelu/api/graphql/types/merchants/permissions.py index b9c981452..d783a1c3e 100644 --- a/tilavarauspalvelu/api/graphql/types/merchants/permissions.py +++ b/tilavarauspalvelu/api/graphql/types/merchants/permissions.py @@ -5,8 +5,8 @@ from query_optimizer.typing import GraphQLFilterInfo from common.typing import AnyUser -from merchants.models import PaymentOrder from tilavarauspalvelu.api.graphql.extensions import error_codes +from tilavarauspalvelu.models import PaymentOrder if TYPE_CHECKING: import uuid diff --git a/tilavarauspalvelu/api/graphql/types/merchants/types.py b/tilavarauspalvelu/api/graphql/types/merchants/types.py index fd6f86103..27d08b1e5 100644 --- a/tilavarauspalvelu/api/graphql/types/merchants/types.py +++ b/tilavarauspalvelu/api/graphql/types/merchants/types.py @@ -4,8 +4,8 @@ from common.date_utils import local_datetime from common.typing import GQLInfo -from merchants.enums import OrderStatus, PaymentType -from merchants.models import PaymentMerchant, PaymentOrder, PaymentProduct +from tilavarauspalvelu.enums import OrderStatus, PaymentType +from tilavarauspalvelu.models import PaymentMerchant, PaymentOrder, PaymentProduct from .permissions import PaymentOrderPermission diff --git a/tilavarauspalvelu/api/graphql/types/reservation/filtersets.py b/tilavarauspalvelu/api/graphql/types/reservation/filtersets.py index 1fcdbbf90..fa96e0f4f 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/filtersets.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/filtersets.py @@ -10,11 +10,11 @@ from common.db import text_search from common.utils import log_text_search -from merchants.enums import OrderStatusWithFree from permissions.enums import UserRoleChoice from reservations.enums import CustomerTypeChoice, ReservationStateChoice, ReservationTypeChoice from reservations.models import Reservation from tilavarauspalvelu.api.graphql.extensions.filters import TimezoneAwareDateFilter +from tilavarauspalvelu.enums import OrderStatusWithFree if TYPE_CHECKING: from common.typing import AnyUser diff --git a/tilavarauspalvelu/api/graphql/types/reservation/mutations.py b/tilavarauspalvelu/api/graphql/types/reservation/mutations.py index 9d69ad5da..5f6867cd6 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/mutations.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/mutations.py @@ -6,11 +6,11 @@ from common.date_utils import local_datetime from common.typing import AnyUser -from merchants.enums import OrderStatus -from merchants.verkkokauppa.order.exceptions import CancelOrderError from reservations.enums import ReservationStateChoice from reservations.models import Reservation from tilavarauspalvelu.api.graphql.types.merchants.types import PaymentOrderNode +from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import CancelOrderError from .permissions import ( ReservationCommentPermission, @@ -38,7 +38,7 @@ from .serializers.staff_reservation_modify_serializers import StaffReservationModifySerializer if TYPE_CHECKING: - from merchants.models import PaymentOrder + from tilavarauspalvelu.models import PaymentOrder __all__ = [ "ReservationCreateMutation", diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/cancellation_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/cancellation_serializers.py index bc5b867bf..51c99fef5 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/cancellation_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/cancellation_serializers.py @@ -6,14 +6,14 @@ from common.date_utils import local_datetime from email_notification.helpers.reservation_email_notification_sender import ReservationEmailNotificationSender -from merchants.enums import OrderStatus from reservations.enums import ReservationStateChoice from reservations.models import Reservation from reservations.tasks import refund_paid_reservation_task from tilavarauspalvelu.api.graphql.extensions import error_codes +from tilavarauspalvelu.enums import OrderStatus if TYPE_CHECKING: - from merchants.models import PaymentOrder + from tilavarauspalvelu.models import PaymentOrder __all__ = [ "ReservationCancellationSerializer", diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/confirm_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/confirm_serializers.py index d42dcb24f..973e341db 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/confirm_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/confirm_serializers.py @@ -4,21 +4,21 @@ from graphene_django_extensions.fields import EnumFriendlyChoiceField from email_notification.helpers.reservation_email_notification_sender import ReservationEmailNotificationSender -from merchants.enums import Language, OrderStatus -from merchants.models import PaymentOrder -from merchants.verkkokauppa.helpers import create_mock_verkkokauppa_order, get_verkkokauppa_order_params -from merchants.verkkokauppa.order.exceptions import CreateOrderError -from merchants.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from reservation_units.enums import PaymentType, PricingType from reservation_units.utils.reservation_unit_pricing_helper import ReservationUnitPricingHelper from reservations.enums import ReservationStateChoice from reservations.models import Reservation from tilavarauspalvelu.api.graphql.extensions.validation_errors import ValidationErrorCodes, ValidationErrorWithCode from tilavarauspalvelu.api.graphql.types.reservation.serializers.update_serializers import ReservationUpdateSerializer +from tilavarauspalvelu.enums import Language, OrderStatus +from tilavarauspalvelu.models import PaymentOrder +from tilavarauspalvelu.utils.verkkokauppa.helpers import create_mock_verkkokauppa_order, get_verkkokauppa_order_params +from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import CreateOrderError +from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from utils.sentry import SentryLogger if TYPE_CHECKING: - from merchants.verkkokauppa.order.types import CreateOrderParams, Order + from tilavarauspalvelu.utils.verkkokauppa.order.types import CreateOrderParams, Order class ReservationConfirmSerializer(ReservationUpdateSerializer): diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/refund_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/refund_serializers.py index 71b54e887..10efdf6ee 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/refund_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/refund_serializers.py @@ -4,13 +4,13 @@ from django.utils.timezone import get_default_timezone from common.utils import comma_sep_str -from merchants.enums import OrderStatus -from merchants.models import PaymentOrder from reservations.enums import ReservationStateChoice from reservations.models import Reservation from reservations.tasks import refund_paid_reservation_task from tilavarauspalvelu.api.graphql.extensions.serializers import OldPrimaryKeySerializer from tilavarauspalvelu.api.graphql.extensions.validation_errors import ValidationErrorCodes, ValidationErrorWithCode +from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.models import PaymentOrder DEFAULT_TIMEZONE = get_default_timezone() diff --git a/tilavarauspalvelu/api/graphql/types/reservation/types.py b/tilavarauspalvelu/api/graphql/types/reservation/types.py index 277bd7381..1a6ca4a87 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/types.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/types.py @@ -10,13 +10,13 @@ from common.db import SubqueryArray from common.typing import AnyUser, GQLInfo from common.utils import ical_hmac_signature -from merchants.models import PaymentOrder from reservation_units.models import ReservationUnit from reservations.enums import CustomerTypeChoice, ReservationStateChoice, ReservationTypeChoice from reservations.enums import ReservationTypeChoice as ReservationTypeField from reservations.models import Reservation from reservations.querysets import ReservationQuerySet from tilavarauspalvelu.api.graphql.types.merchants.types import PaymentOrderNode +from tilavarauspalvelu.models import PaymentOrder from users.models import User from .filtersets import ReservationFilterSet diff --git a/tilavarauspalvelu/api/graphql/types/reservation_unit/types.py b/tilavarauspalvelu/api/graphql/types/reservation_unit/types.py index 6a14ab539..d39800530 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation_unit/types.py +++ b/tilavarauspalvelu/api/graphql/types/reservation_unit/types.py @@ -13,7 +13,6 @@ from common.db import SubqueryCount from common.typing import GQLInfo -from merchants.models import PaymentMerchant from opening_hours.models import OriginHaukiResource from opening_hours.utils.hauki_link_generator import generate_hauki_link from reservation_units.enums import ReservationUnitPublishingState, ReservationUnitReservationState @@ -23,6 +22,7 @@ from spaces.models import Location, Space, Unit from tilavarauspalvelu.api.graphql.types.location.types import LocationNode from tilavarauspalvelu.api.graphql.types.reservation.types import ReservationNode +from tilavarauspalvelu.models import PaymentMerchant from .filtersets import ReservationUnitFilterSet from .permissions import ReservationUnitPermission diff --git a/merchants/mock_verkkokauppa_api/__init__.py b/tilavarauspalvelu/api/mock_verkkokauppa_api/__init__.py similarity index 100% rename from merchants/mock_verkkokauppa_api/__init__.py rename to tilavarauspalvelu/api/mock_verkkokauppa_api/__init__.py diff --git a/merchants/mock_verkkokauppa_api/urls.py b/tilavarauspalvelu/api/mock_verkkokauppa_api/urls.py similarity index 83% rename from merchants/mock_verkkokauppa_api/urls.py rename to tilavarauspalvelu/api/mock_verkkokauppa_api/urls.py index 0a7e230aa..2a77eadd2 100644 --- a/merchants/mock_verkkokauppa_api/urls.py +++ b/tilavarauspalvelu/api/mock_verkkokauppa_api/urls.py @@ -1,7 +1,7 @@ from django.urls import path from django.views.decorators.csrf import csrf_exempt -from merchants.mock_verkkokauppa_api.views import MockVerkkokauppaView +from .views import MockVerkkokauppaView mock_verkkokauppa_view = csrf_exempt(MockVerkkokauppaView.as_view()) # NOSONAR diff --git a/merchants/mock_verkkokauppa_api/views.py b/tilavarauspalvelu/api/mock_verkkokauppa_api/views.py similarity index 94% rename from merchants/mock_verkkokauppa_api/views.py rename to tilavarauspalvelu/api/mock_verkkokauppa_api/views.py index 75d2caf22..ee2e71362 100644 --- a/merchants/mock_verkkokauppa_api/views.py +++ b/tilavarauspalvelu/api/mock_verkkokauppa_api/views.py @@ -9,14 +9,19 @@ from django.views.generic import TemplateView from common.date_utils import local_datetime -from merchants.enums import OrderStatus -from merchants.models import PaymentOrder from reservations.enums import ReservationStateChoice +from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.models import PaymentOrder if TYPE_CHECKING: from reservations.models import Reservation +__all__ = [ + "MockVerkkokauppaView", +] + + class MockVerkkokauppaView(TemplateView): template_name = "mock_verkkokauppa/index.html" diff --git a/tilavarauspalvelu/api/webhooks/views.py b/tilavarauspalvelu/api/webhooks/views.py index 12b4c5c1a..53e2cbf6e 100644 --- a/tilavarauspalvelu/api/webhooks/views.py +++ b/tilavarauspalvelu/api/webhooks/views.py @@ -5,12 +5,12 @@ from rest_framework.response import Response from common.date_utils import local_datetime -from merchants.enums import OrderStatus -from merchants.models import PaymentOrder -from merchants.verkkokauppa.order.exceptions import GetOrderError -from merchants.verkkokauppa.payment.exceptions import GetPaymentError, GetRefundStatusError -from merchants.verkkokauppa.payment.types import PaymentStatus, RefundStatus -from merchants.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient +from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.models import PaymentOrder +from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import GetOrderError +from tilavarauspalvelu.utils.verkkokauppa.payment.exceptions import GetPaymentError, GetRefundStatusError +from tilavarauspalvelu.utils.verkkokauppa.payment.types import PaymentStatus, RefundStatus +from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from utils.sentry import SentryLogger from .permissions import WebhookPermission diff --git a/tilavarauspalvelu/enums.py b/tilavarauspalvelu/enums.py index e6b2c2915..0fd578352 100644 --- a/tilavarauspalvelu/enums.py +++ b/tilavarauspalvelu/enums.py @@ -1,7 +1,15 @@ +from __future__ import annotations + from django.db import models +from django.utils.functional import classproperty from django.utils.translation import gettext_lazy as _ +from django.utils.translation import pgettext_lazy __all__ = [ + "Language", + "OrderStatus", + "OrderStatusWithFree", + "PaymentType", "ServiceTypeChoices", "TermsOfUseTypeChoices", ] @@ -20,3 +28,53 @@ class TermsOfUseTypeChoices(models.TextChoices): RECURRING = "recurring_terms", _("Recurring reservation terms") SERVICE = "service_terms", _("Service-specific terms") PRICING = "pricing_terms", _("Pricing terms") + + +class Language(models.TextChoices): + FI = "fi", _("Finnish") + SV = "sv", _("Swedish") + EN = "en", _("English") + + +class OrderStatus(models.TextChoices): + DRAFT = "DRAFT", pgettext_lazy("OrderStatus", "Draft") # Unpaid order + EXPIRED = "EXPIRED", pgettext_lazy("OrderStatus", "Expired") + CANCELLED = "CANCELLED", pgettext_lazy("OrderStatus", "Cancelled") + PAID = "PAID", pgettext_lazy("OrderStatus", "Paid") + PAID_MANUALLY = "PAID_MANUALLY", pgettext_lazy("OrderStatus", "Paid manually") + REFUNDED = "REFUNDED", pgettext_lazy("OrderStatus", "Refunded") + + @classproperty + def needs_update_statuses(cls) -> list[OrderStatus]: + return [ + OrderStatus.DRAFT, + OrderStatus.EXPIRED, + OrderStatus.CANCELLED, + ] + + @classproperty + def can_be_cancelled_statuses(cls) -> list[OrderStatus]: + return [ + OrderStatus.DRAFT, + OrderStatus.EXPIRED, + ] + + +class OrderStatusWithFree(models.TextChoices): + """Same as OrderStatus, but includes the 'FREE' option used for filtering reservations without payments.""" + + # Note: Enums cannot be subclassed, so we have to redefine all "original" members. + DRAFT = "DRAFT", pgettext_lazy("OrderStatus", "Draft") + EXPIRED = "EXPIRED", pgettext_lazy("OrderStatus", "Expired") + CANCELLED = "CANCELLED", pgettext_lazy("OrderStatus", "Cancelled") + PAID = "PAID", pgettext_lazy("OrderStatus", "Paid") + PAID_MANUALLY = "PAID_MANUALLY", pgettext_lazy("OrderStatus", "Paid manually") + REFUNDED = "REFUNDED", pgettext_lazy("OrderStatus", "Refunded") + + FREE = "FREE", pgettext_lazy("OrderStatus", "Free") + + +class PaymentType(models.TextChoices): + ON_SITE = "ON_SITE", pgettext_lazy("PaymentType", "On site") + ONLINE = "ONLINE", pgettext_lazy("PaymentType", "Online") + INVOICE = "INVOICE", pgettext_lazy("PaymentType", "Invoice") diff --git a/tilavarauspalvelu/migrations/0003_merchants.py b/tilavarauspalvelu/migrations/0003_merchants.py new file mode 100644 index 000000000..27b0cebe5 --- /dev/null +++ b/tilavarauspalvelu/migrations/0003_merchants.py @@ -0,0 +1,181 @@ +# Generated by Django 5.1.1 on 2024-09-18 10:41 + +import django.db.models.deletion +from django.db import migrations, models + +import tilavarauspalvelu.utils.validators + + +class Migration(migrations.Migration): + dependencies = [ + ("reservations", "0082_remove_net_prices"), + ("tilavarauspalvelu", "0002_termsofuse"), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.CreateModel( + name="PaymentAccounting", + fields=[ + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("name", models.CharField(max_length=128)), + ( + "company_code", + models.CharField(max_length=4, validators=[tilavarauspalvelu.utils.validators.is_numeric]), + ), + ( + "main_ledger_account", + models.CharField(max_length=6, validators=[tilavarauspalvelu.utils.validators.is_numeric]), + ), + ("vat_code", models.CharField(max_length=2)), + ( + "internal_order", + models.CharField( + blank=True, + default="", + max_length=10, + validators=[tilavarauspalvelu.utils.validators.is_numeric], + ), + ), + ( + "profit_center", + models.CharField( + blank=True, + default="", + max_length=7, + validators=[tilavarauspalvelu.utils.validators.is_numeric], + ), + ), + ( + "project", + models.CharField( + blank=True, + default="", + max_length=16, + validators=[ + tilavarauspalvelu.utils.validators.validate_accounting_project, + tilavarauspalvelu.utils.validators.is_numeric, + ], + ), + ), + ( + "operation_area", + models.CharField( + blank=True, + default="", + max_length=6, + validators=[tilavarauspalvelu.utils.validators.is_numeric], + ), + ), + ("balance_profit_center", models.CharField(max_length=10)), + ], + options={ + "db_table": "payment_accounting", + "ordering": ["pk"], + "base_manager_name": "objects", + }, + ), + migrations.CreateModel( + name="PaymentMerchant", + fields=[ + ("id", models.UUIDField(primary_key=True, serialize=False)), + ("name", models.CharField(max_length=128)), + ], + options={ + "db_table": "payment_merchant", + "ordering": ["pk"], + "base_manager_name": "objects", + }, + ), + migrations.CreateModel( + name="PaymentOrder", + fields=[ + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("remote_id", models.UUIDField(blank=True, null=True)), + ("payment_id", models.CharField(blank=True, default="", max_length=128)), + ("refund_id", models.UUIDField(blank=True, null=True)), + ( + "payment_type", + models.CharField( + choices=[("ON_SITE", "On site"), ("ONLINE", "Online"), ("INVOICE", "Invoice")], + max_length=128, + ), + ), + ( + "status", + models.CharField( + choices=[ + ("DRAFT", "Draft"), + ("EXPIRED", "Expired"), + ("CANCELLED", "Cancelled"), + ("PAID", "Paid"), + ("PAID_MANUALLY", "Paid manually"), + ("REFUNDED", "Refunded"), + ], + db_index=True, + max_length=128, + ), + ), + ("price_net", models.DecimalField(decimal_places=2, max_digits=10)), + ("price_vat", models.DecimalField(decimal_places=2, max_digits=10)), + ("price_total", models.DecimalField(decimal_places=2, max_digits=10)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("processed_at", models.DateTimeField(blank=True, null=True)), + ( + "language", + models.CharField( + choices=[("fi", "Finnish"), ("sv", "Swedish"), ("en", "English")], max_length=8 + ), + ), + ("reservation_user_uuid", models.UUIDField(blank=True, null=True)), + ("checkout_url", models.CharField(blank=True, default="", max_length=512)), + ("receipt_url", models.CharField(blank=True, default="", max_length=512)), + ( + "reservation", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="payment_order", + to="reservations.reservation", + ), + ), + ], + options={ + "db_table": "payment_order", + "ordering": ["pk"], + "base_manager_name": "objects", + }, + ), + migrations.CreateModel( + name="PaymentProduct", + fields=[ + ("id", models.UUIDField(primary_key=True, serialize=False)), + ( + "merchant", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="products", + to="tilavarauspalvelu.paymentmerchant", + ), + ), + ], + options={ + "db_table": "payment_product", + "ordering": ["pk"], + "base_manager_name": "objects", + }, + ), + ], + # We just moved the model, so no need create a new table + database_operations=[], + ), + ] diff --git a/tilavarauspalvelu/models/__init__.py b/tilavarauspalvelu/models/__init__.py index a962f403d..13ce3013c 100644 --- a/tilavarauspalvelu/models/__init__.py +++ b/tilavarauspalvelu/models/__init__.py @@ -1,7 +1,15 @@ +from .payment_accounting.model import PaymentAccounting +from .payment_merchant.model import PaymentMerchant +from .payment_order.model import PaymentOrder +from .payment_product.model import PaymentProduct from .service.model import Service from .terms_of_use.model import TermsOfUse __all__ = [ + "PaymentAccounting", + "PaymentMerchant", + "PaymentOrder", + "PaymentProduct", "Service", "TermsOfUse", ] diff --git a/tilavarauspalvelu/models/payment_accounting/actions.py b/tilavarauspalvelu/models/payment_accounting/actions.py index e69de29bb..e09d33d72 100644 --- a/tilavarauspalvelu/models/payment_accounting/actions.py +++ b/tilavarauspalvelu/models/payment_accounting/actions.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .model import PaymentAccounting + +__all__ = [ + "PaymentAccountingActions", +] + + +class PaymentAccountingActions: + def __init__(self, payment_accounting: PaymentAccounting) -> None: + self.payment_accounting = payment_accounting diff --git a/tilavarauspalvelu/models/payment_accounting/model.py b/tilavarauspalvelu/models/payment_accounting/model.py index e69de29bb..8e71ff023 100644 --- a/tilavarauspalvelu/models/payment_accounting/model.py +++ b/tilavarauspalvelu/models/payment_accounting/model.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING, Any + +from django.conf import settings +from django.core.exceptions import ValidationError +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from tilavarauspalvelu.utils.validators import is_numeric, validate_accounting_project + +from .queryset import PaymentAccountingQuerySet + +if TYPE_CHECKING: + from .actions import PaymentAccountingActions + + +__all__ = [ + "PaymentAccounting", +] + + +class PaymentAccounting(models.Model): + """Custom validation comes from requirements in SAP""" + + name: str = models.CharField(max_length=128) + company_code: str = models.CharField(max_length=4, validators=[is_numeric]) + main_ledger_account: str = models.CharField(max_length=6, validators=[is_numeric]) + vat_code: str = models.CharField(max_length=2) + internal_order: str = models.CharField(blank=True, default="", max_length=10, validators=[is_numeric]) + profit_center: str = models.CharField(blank=True, default="", max_length=7, validators=[is_numeric]) + project: str = models.CharField( + blank=True, + default="", + max_length=16, + validators=[validate_accounting_project, is_numeric], + ) + operation_area: str = models.CharField(blank=True, default="", max_length=6, validators=[is_numeric]) + balance_profit_center: str = models.CharField(max_length=10) + + objects = PaymentAccountingQuerySet.as_manager() + + class Meta: + db_table = "payment_accounting" + base_manager_name = "objects" + ordering = ["pk"] + + def __str__(self) -> str: + return self.name + + def save(self, *args: Any, **kwargs: Any) -> None: + from reservation_units.models import ReservationUnit + from reservation_units.tasks import refresh_reservation_unit_accounting + + super().save(*args, **kwargs) + + if settings.UPDATE_ACCOUNTING: + reservation_units_from_units = ReservationUnit.objects.filter(unit__in=self.units.all()) + reservation_units = reservation_units_from_units.union(self.reservation_units.all()) + for reservation_unit in reservation_units: + refresh_reservation_unit_accounting.delay(reservation_unit.pk) + + @cached_property + def actions(self) -> PaymentAccountingActions: + # Import actions inline to defer loading them. + # This allows us to avoid circular imports. + from .actions import PaymentAccountingActions + + return PaymentAccountingActions(self) + + def clean(self) -> None: + if not self.project and not self.profit_center and not self.internal_order: + error_message = _("One of the following fields must be given: internal_order, profit_center, project") + raise ValidationError( + { + "internal_order": [error_message], + "profit_center": [error_message], + "project": [error_message], + } + ) diff --git a/tilavarauspalvelu/models/payment_accounting/queryset.py b/tilavarauspalvelu/models/payment_accounting/queryset.py index e69de29bb..993c33abb 100644 --- a/tilavarauspalvelu/models/payment_accounting/queryset.py +++ b/tilavarauspalvelu/models/payment_accounting/queryset.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from django.db import models + +__all__ = [ + "PaymentAccountingQuerySet", +] + + +class PaymentAccountingQuerySet(models.QuerySet): ... diff --git a/tilavarauspalvelu/models/payment_merchant/actions.py b/tilavarauspalvelu/models/payment_merchant/actions.py index e69de29bb..6fd869ea2 100644 --- a/tilavarauspalvelu/models/payment_merchant/actions.py +++ b/tilavarauspalvelu/models/payment_merchant/actions.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .model import PaymentMerchant + +__all__ = [ + "PaymentMerchantActions", +] + + +class PaymentMerchantActions: + def __init__(self, payment_merchant: PaymentMerchant) -> None: + self.payment_merchant = payment_merchant diff --git a/tilavarauspalvelu/models/payment_merchant/model.py b/tilavarauspalvelu/models/payment_merchant/model.py index e69de29bb..95acf6685 100644 --- a/tilavarauspalvelu/models/payment_merchant/model.py +++ b/tilavarauspalvelu/models/payment_merchant/model.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING + +from django.db import models + +from .queryset import PaymentMerchantQuerySet + +if TYPE_CHECKING: + import uuid + + from .actions import PaymentMerchantActions + + +__all__ = [ + "PaymentMerchant", +] + + +class PaymentMerchant(models.Model): + """ + ID is not auto-generated. It comes from the Merchant experience API. See admin.py. + https://checkout-test-api.test.hel.ninja/v1/merchant/docs/swagger-ui/ + """ + + id: uuid.UUID = models.UUIDField(primary_key=True) + name: str = models.CharField(max_length=128) + + objects = PaymentMerchantQuerySet.as_manager() + + class Meta: + db_table = "payment_merchant" + base_manager_name = "objects" + ordering = ["pk"] + + def __str__(self) -> str: + return self.name + + @cached_property + def actions(self) -> PaymentMerchantActions: + # Import actions inline to defer loading them. + # This allows us to avoid circular imports. + from .actions import PaymentMerchantActions + + return PaymentMerchantActions(self) diff --git a/tilavarauspalvelu/models/payment_merchant/queryset.py b/tilavarauspalvelu/models/payment_merchant/queryset.py index e69de29bb..85c93b97a 100644 --- a/tilavarauspalvelu/models/payment_merchant/queryset.py +++ b/tilavarauspalvelu/models/payment_merchant/queryset.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from django.db import models + +__all__ = [ + "PaymentMerchantQuerySet", +] + + +class PaymentMerchantQuerySet(models.QuerySet): ... diff --git a/tilavarauspalvelu/models/payment_order/actions.py b/tilavarauspalvelu/models/payment_order/actions.py index e69de29bb..4e41002ae 100644 --- a/tilavarauspalvelu/models/payment_order/actions.py +++ b/tilavarauspalvelu/models/payment_order/actions.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .model import PaymentOrder + +__all__ = [ + "PaymentOrderActions", +] + + +class PaymentOrderActions: + def __init__(self, payment_order: PaymentOrder) -> None: + self.payment_order = payment_order diff --git a/tilavarauspalvelu/models/payment_order/model.py b/tilavarauspalvelu/models/payment_order/model.py index e69de29bb..73d3d9905 100644 --- a/tilavarauspalvelu/models/payment_order/model.py +++ b/tilavarauspalvelu/models/payment_order/model.py @@ -0,0 +1,183 @@ +from __future__ import annotations + +import datetime +from decimal import Decimal +from functools import cached_property +from typing import TYPE_CHECKING, Any + +from django.conf import settings +from django.core.exceptions import ValidationError +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from common.date_utils import local_datetime +from email_notification.helpers.reservation_email_notification_sender import ReservationEmailNotificationSender +from reservations.enums import ReservationStateChoice +from tilavarauspalvelu.enums import Language, OrderStatus, PaymentType +from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import CancelOrderError +from tilavarauspalvelu.utils.verkkokauppa.payment.exceptions import GetPaymentError +from tilavarauspalvelu.utils.verkkokauppa.payment.types import Payment +from tilavarauspalvelu.utils.verkkokauppa.payment.types import PaymentStatus as WebShopPaymentStatus +from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient +from utils.sentry import SentryLogger + +from .queryset import PaymentOrderQuerySet + +if TYPE_CHECKING: + import uuid + + from reservations.models import Reservation + from tilavarauspalvelu.utils.verkkokauppa.order.types import Order + + from .actions import PaymentOrderActions + + +__all__ = [ + "PaymentOrder", +] + + +class PaymentOrder(models.Model): + reservation: Reservation | None = models.ForeignKey( + "reservations.Reservation", + related_name="payment_order", + on_delete=models.SET_NULL, + null=True, + blank=True, + ) + + remote_id: uuid.UUID | None = models.UUIDField(blank=True, null=True) + payment_id: str = models.CharField(blank=True, default="", max_length=128) + refund_id: uuid.UUID | None = models.UUIDField(blank=True, null=True) + payment_type: str = models.CharField(max_length=128, choices=PaymentType.choices) + status: str = models.CharField(max_length=128, choices=OrderStatus.choices, db_index=True) + + price_net: Decimal = models.DecimalField(max_digits=10, decimal_places=2) + price_vat: Decimal = models.DecimalField(max_digits=10, decimal_places=2) + price_total: Decimal = models.DecimalField(max_digits=10, decimal_places=2) + + created_at: datetime.datetime = models.DateTimeField(auto_now_add=True) + processed_at: datetime.datetime | None = models.DateTimeField(null=True, blank=True) + + language: str = models.CharField(max_length=8, choices=Language.choices) + reservation_user_uuid: uuid.UUID | None = models.UUIDField(blank=True, null=True) + checkout_url: str = models.CharField(blank=True, default="", max_length=512) + receipt_url: str = models.CharField(blank=True, default="", max_length=512) + + objects = PaymentOrderQuerySet.as_manager() + + class Meta: + db_table = "payment_order" + base_manager_name = "objects" + ordering = ["pk"] + + def __str__(self) -> str: + return f"PaymentOrder {self.pk}" + + def save(self, *args: Any, **kwargs: Any) -> PaymentOrder: + self.full_clean() + return super().save(*args, **kwargs) + + @cached_property + def actions(self) -> PaymentOrderActions: + # Import actions inline to defer loading them. + # This allows us to avoid circular imports. + from .actions import PaymentOrderActions + + return PaymentOrderActions(self) + + def clean(self) -> None: + validation_errors = {} + + failsafe_price_net = self.price_net or Decimal("0.0") + failsafe_price_vat = self.price_vat or Decimal("0.0") + + if self.price_net is not None and self.price_net < Decimal("0.01"): + validation_errors.setdefault("price_net", []).append(_("Must be greater than 0.01")) + if self.price_vat is not None and self.price_vat < Decimal("0"): + validation_errors.setdefault("price_vat", []).append(_("Must be greater than 0")) + if self.price_total is not None and self.price_total != failsafe_price_net + failsafe_price_vat: + validation_errors.setdefault("price_total", []).append(_("Must be the sum of net and vat amounts")) + + if validation_errors: + raise ValidationError(validation_errors) + + @property + def expires_at(self) -> datetime.datetime | None: + if self.status != OrderStatus.DRAFT: + return None + + return self.created_at + datetime.timedelta(minutes=settings.VERKKOKAUPPA_ORDER_EXPIRATION_MINUTES) + + def get_order_payment_from_webshop(self) -> Payment | None: + try: + return VerkkokauppaAPIClient.get_payment(order_uuid=self.remote_id) + except GetPaymentError as err: + SentryLogger.log_exception(err, details="Fetching order payment failed.", remote_id=self.remote_id) + raise + + def cancel_order_in_webshop(self) -> Order | None: + try: + return VerkkokauppaAPIClient.cancel_order(order_uuid=self.remote_id, user_uuid=self.reservation_user_uuid) + except CancelOrderError as err: + SentryLogger.log_exception(err, details="Canceling order failed.", remote_id=self.remote_id) + raise + + def get_order_status_from_webshop_response(self, webshop_payment: Payment | None) -> OrderStatus: + """Determines the order status based on the payment response from the webshop.""" + # Statuses PAID, PAID_MANUALLY and REFUNDED are "final" and should not be updated from the webshop. + if self.status in (OrderStatus.REFUNDED, OrderStatus.PAID, OrderStatus.PAID_MANUALLY): + return OrderStatus(self.status) + + older_than_minutes = settings.VERKKOKAUPPA_ORDER_EXPIRATION_MINUTES + webshop_payment_expires_at = local_datetime() - datetime.timedelta(minutes=older_than_minutes) + + if webshop_payment: + if webshop_payment.status == WebShopPaymentStatus.CANCELLED.value: + return OrderStatus.CANCELLED + if webshop_payment.status == WebShopPaymentStatus.PAID_ONLINE.value: + return OrderStatus.PAID + if ( + webshop_payment.status == WebShopPaymentStatus.CREATED.value + and webshop_payment.timestamp + and webshop_payment.timestamp > webshop_payment_expires_at + ): + # User has entered payment phase in webshop (Payment is created but not yet paid), + # give more time to complete the payment before marking the order as expired. + return OrderStatus.DRAFT + elif not webshop_payment and self.expires_at > local_datetime(): + # User has not entered payment phase in webshop within the expiration time + return OrderStatus.DRAFT + + return OrderStatus.EXPIRED + + def update_order_status(self, new_status: OrderStatus, payment_id: str = "") -> None: + """ + Updates the PaymentOrder status and processed_at timestamp if the status has changed. + + If the order is paid, updates the reservation state to confirmed and sends a confirmation email. + """ + if new_status == self.status: + return + + self.status = new_status + self.processed_at = local_datetime() + if payment_id: + self.payment_id = payment_id + self.save(update_fields=["status", "processed_at", "payment_id"]) + + # If the order is paid, update the reservation state to confirmed and send confirmation email + if ( + self.status == OrderStatus.PAID + and self.reservation is not None + and self.reservation.state == ReservationStateChoice.WAITING_FOR_PAYMENT + ): + self.reservation.state = ReservationStateChoice.CONFIRMED + self.reservation.save(update_fields=["state"]) + ReservationEmailNotificationSender.send_confirmation_email(reservation=self.reservation) + + def refresh_order_status_from_webshop(self): + """Fetches the payment status from the webshop and updates the PaymentOrder status accordingly.""" + webshop_payment: Payment | None = self.get_order_payment_from_webshop() + new_status: OrderStatus = self.get_order_status_from_webshop_response(webshop_payment) + self.update_order_status(new_status, webshop_payment.payment_id if webshop_payment else "") diff --git a/tilavarauspalvelu/models/payment_order/queryset.py b/tilavarauspalvelu/models/payment_order/queryset.py index e69de29bb..43c35e34b 100644 --- a/tilavarauspalvelu/models/payment_order/queryset.py +++ b/tilavarauspalvelu/models/payment_order/queryset.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from django.db import models + +__all__ = [ + "PaymentOrderQuerySet", +] + + +class PaymentOrderQuerySet(models.QuerySet): ... diff --git a/tilavarauspalvelu/models/payment_product/actions.py b/tilavarauspalvelu/models/payment_product/actions.py index e69de29bb..6caa115ea 100644 --- a/tilavarauspalvelu/models/payment_product/actions.py +++ b/tilavarauspalvelu/models/payment_product/actions.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .model import PaymentProduct + +__all__ = [ + "PaymentProductActions", +] + + +class PaymentProductActions: + def __init__(self, payment_product: PaymentProduct) -> None: + self.payment_product = payment_product diff --git a/tilavarauspalvelu/models/payment_product/model.py b/tilavarauspalvelu/models/payment_product/model.py index e69de29bb..3d374fb9f 100644 --- a/tilavarauspalvelu/models/payment_product/model.py +++ b/tilavarauspalvelu/models/payment_product/model.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING + +from django.db import models + +from .queryset import PaymentProductQuerySet + +if TYPE_CHECKING: + import uuid + + from tilavarauspalvelu.models import PaymentMerchant + + from .actions import PaymentProductActions + + +__all__ = [ + "PaymentProduct", +] + + +class PaymentProduct(models.Model): + id: uuid.UUID = models.UUIDField(primary_key=True) + + merchant: PaymentMerchant | None = models.ForeignKey( + "tilavarauspalvelu.PaymentMerchant", + related_name="products", + on_delete=models.PROTECT, + null=True, + ) + + objects = PaymentProductQuerySet.as_manager() + + class Meta: + db_table = "payment_product" + base_manager_name = "objects" + ordering = ["pk"] + + def __str__(self) -> str: + return str(self.id) + + @cached_property + def actions(self) -> PaymentProductActions: + # Import actions inline to defer loading them. + # This allows us to avoid circular imports. + from .actions import PaymentProductActions + + return PaymentProductActions(self) diff --git a/tilavarauspalvelu/models/payment_product/queryset.py b/tilavarauspalvelu/models/payment_product/queryset.py index e69de29bb..e54eb2a30 100644 --- a/tilavarauspalvelu/models/payment_product/queryset.py +++ b/tilavarauspalvelu/models/payment_product/queryset.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from django.db import models + +__all__ = [ + "PaymentProductQuerySet", +] + + +class PaymentProductQuerySet(models.QuerySet): ... diff --git a/merchants/validators.py b/tilavarauspalvelu/utils/validators.py similarity index 100% rename from merchants/validators.py rename to tilavarauspalvelu/utils/validators.py diff --git a/merchants/verkkokauppa/__init__.py b/tilavarauspalvelu/utils/verkkokauppa/__init__.py similarity index 100% rename from merchants/verkkokauppa/__init__.py rename to tilavarauspalvelu/utils/verkkokauppa/__init__.py diff --git a/merchants/verkkokauppa/constants.py b/tilavarauspalvelu/utils/verkkokauppa/constants.py similarity index 100% rename from merchants/verkkokauppa/constants.py rename to tilavarauspalvelu/utils/verkkokauppa/constants.py diff --git a/merchants/verkkokauppa/exceptions.py b/tilavarauspalvelu/utils/verkkokauppa/exceptions.py similarity index 100% rename from merchants/verkkokauppa/exceptions.py rename to tilavarauspalvelu/utils/verkkokauppa/exceptions.py diff --git a/merchants/verkkokauppa/helpers.py b/tilavarauspalvelu/utils/verkkokauppa/helpers.py similarity index 96% rename from merchants/verkkokauppa/helpers.py rename to tilavarauspalvelu/utils/verkkokauppa/helpers.py index 84e14e0c4..919599dc8 100644 --- a/merchants/verkkokauppa/helpers.py +++ b/tilavarauspalvelu/utils/verkkokauppa/helpers.py @@ -8,17 +8,17 @@ from common.date_utils import local_datetime from config.utils.date_util import localized_short_weekday -from merchants.models import PaymentMerchant, PaymentProduct -from merchants.verkkokauppa.exceptions import UnsupportedMetaKeyError -from merchants.verkkokauppa.order.types import ( +from reservation_units.utils.reservation_unit_payment_helper import ReservationUnitPaymentHelper +from reservations.models import Reservation +from tilavarauspalvelu.models import PaymentMerchant, PaymentProduct +from tilavarauspalvelu.utils.verkkokauppa.exceptions import UnsupportedMetaKeyError +from tilavarauspalvelu.utils.verkkokauppa.order.types import ( CreateOrderParams, Order, OrderCustomer, OrderItemMetaParams, OrderItemParams, ) -from reservation_units.utils.reservation_unit_payment_helper import ReservationUnitPaymentHelper -from reservations.models import Reservation def parse_datetime(string: str | None) -> datetime | None: diff --git a/merchants/verkkokauppa/merchants/__init__.py b/tilavarauspalvelu/utils/verkkokauppa/merchants/__init__.py similarity index 100% rename from merchants/verkkokauppa/merchants/__init__.py rename to tilavarauspalvelu/utils/verkkokauppa/merchants/__init__.py diff --git a/merchants/verkkokauppa/merchants/exceptions.py b/tilavarauspalvelu/utils/verkkokauppa/merchants/exceptions.py similarity index 78% rename from merchants/verkkokauppa/merchants/exceptions.py rename to tilavarauspalvelu/utils/verkkokauppa/merchants/exceptions.py index 997335ec1..9d2b7e9c6 100644 --- a/merchants/verkkokauppa/merchants/exceptions.py +++ b/tilavarauspalvelu/utils/verkkokauppa/merchants/exceptions.py @@ -1,4 +1,4 @@ -from merchants.verkkokauppa.exceptions import VerkkokauppaError +from tilavarauspalvelu.utils.verkkokauppa.exceptions import VerkkokauppaError class CreateMerchantError(VerkkokauppaError): diff --git a/merchants/verkkokauppa/merchants/types.py b/tilavarauspalvelu/utils/verkkokauppa/merchants/types.py similarity index 95% rename from merchants/verkkokauppa/merchants/types.py rename to tilavarauspalvelu/utils/verkkokauppa/merchants/types.py index 6cdc4d096..5b1b8b854 100644 --- a/merchants/verkkokauppa/merchants/types.py +++ b/tilavarauspalvelu/utils/verkkokauppa/merchants/types.py @@ -3,7 +3,7 @@ from datetime import datetime from typing import Any -from merchants.verkkokauppa.merchants.exceptions import ParseMerchantError, ParseMerchantInfoError +from tilavarauspalvelu.utils.verkkokauppa.merchants.exceptions import ParseMerchantError, ParseMerchantInfoError @dataclass(frozen=True) @@ -57,7 +57,7 @@ class Merchant: @classmethod def from_json(cls, json: dict[str, Any]) -> "Merchant": - from merchants.verkkokauppa.helpers import parse_datetime + from tilavarauspalvelu.utils.verkkokauppa.helpers import parse_datetime try: configurations = json["configurations"] diff --git a/merchants/verkkokauppa/order/__init__.py b/tilavarauspalvelu/utils/verkkokauppa/order/__init__.py similarity index 100% rename from merchants/verkkokauppa/order/__init__.py rename to tilavarauspalvelu/utils/verkkokauppa/order/__init__.py diff --git a/merchants/verkkokauppa/order/exceptions.py b/tilavarauspalvelu/utils/verkkokauppa/order/exceptions.py similarity index 74% rename from merchants/verkkokauppa/order/exceptions.py rename to tilavarauspalvelu/utils/verkkokauppa/order/exceptions.py index 4cc9346d1..7971babb9 100644 --- a/merchants/verkkokauppa/order/exceptions.py +++ b/tilavarauspalvelu/utils/verkkokauppa/order/exceptions.py @@ -1,4 +1,4 @@ -from merchants.verkkokauppa.exceptions import VerkkokauppaError +from tilavarauspalvelu.utils.verkkokauppa.exceptions import VerkkokauppaError class OrderError(VerkkokauppaError): diff --git a/merchants/verkkokauppa/order/types.py b/tilavarauspalvelu/utils/verkkokauppa/order/types.py similarity index 98% rename from merchants/verkkokauppa/order/types.py rename to tilavarauspalvelu/utils/verkkokauppa/order/types.py index e001b4a6b..8cf4194fd 100644 --- a/merchants/verkkokauppa/order/types.py +++ b/tilavarauspalvelu/utils/verkkokauppa/order/types.py @@ -6,7 +6,7 @@ from django.conf import settings -from merchants.verkkokauppa.order.exceptions import ParseOrderError +from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import ParseOrderError from utils.decimal_utils import round_decimal from utils.sentry import SentryLogger @@ -101,7 +101,7 @@ class Order: @classmethod def from_json(cls, json: dict[str, Any]) -> "Order": - from merchants.verkkokauppa.helpers import parse_datetime + from tilavarauspalvelu.utils.verkkokauppa.helpers import parse_datetime subscription_id = json.get("subscriptionId") try: diff --git a/merchants/verkkokauppa/payment/__init__.py b/tilavarauspalvelu/utils/verkkokauppa/payment/__init__.py similarity index 100% rename from merchants/verkkokauppa/payment/__init__.py rename to tilavarauspalvelu/utils/verkkokauppa/payment/__init__.py diff --git a/merchants/verkkokauppa/payment/exceptions.py b/tilavarauspalvelu/utils/verkkokauppa/payment/exceptions.py similarity index 82% rename from merchants/verkkokauppa/payment/exceptions.py rename to tilavarauspalvelu/utils/verkkokauppa/payment/exceptions.py index baa244034..950be7b51 100644 --- a/merchants/verkkokauppa/payment/exceptions.py +++ b/tilavarauspalvelu/utils/verkkokauppa/payment/exceptions.py @@ -1,4 +1,4 @@ -from merchants.verkkokauppa.exceptions import VerkkokauppaError +from tilavarauspalvelu.utils.verkkokauppa.exceptions import VerkkokauppaError class PaymentError(VerkkokauppaError): diff --git a/merchants/verkkokauppa/payment/types.py b/tilavarauspalvelu/utils/verkkokauppa/payment/types.py similarity index 93% rename from merchants/verkkokauppa/payment/types.py rename to tilavarauspalvelu/utils/verkkokauppa/payment/types.py index 5b4fa6004..94e298bf7 100644 --- a/merchants/verkkokauppa/payment/types.py +++ b/tilavarauspalvelu/utils/verkkokauppa/payment/types.py @@ -7,7 +7,11 @@ from django.conf import settings -from merchants.verkkokauppa.payment.exceptions import ParsePaymentError, ParseRefundError, ParseRefundStatusError +from tilavarauspalvelu.utils.verkkokauppa.payment.exceptions import ( + ParsePaymentError, + ParseRefundError, + ParseRefundStatusError, +) from utils.sentry import SentryLogger @@ -93,7 +97,7 @@ class Refund: @classmethod def from_json(cls, json: dict[str, Any]) -> "Refund": - from merchants.verkkokauppa.helpers import parse_datetime + from tilavarauspalvelu.utils.verkkokauppa.helpers import parse_datetime try: return Refund( @@ -125,7 +129,7 @@ class RefundStatusResult: @classmethod def from_json(cls, json: dict[str, Any]) -> "RefundStatusResult": - from merchants.verkkokauppa.helpers import parse_datetime + from tilavarauspalvelu.utils.verkkokauppa.helpers import parse_datetime try: return RefundStatusResult( diff --git a/merchants/verkkokauppa/product/__init__.py b/tilavarauspalvelu/utils/verkkokauppa/product/__init__.py similarity index 100% rename from merchants/verkkokauppa/product/__init__.py rename to tilavarauspalvelu/utils/verkkokauppa/product/__init__.py diff --git a/merchants/verkkokauppa/product/exceptions.py b/tilavarauspalvelu/utils/verkkokauppa/product/exceptions.py similarity index 80% rename from merchants/verkkokauppa/product/exceptions.py rename to tilavarauspalvelu/utils/verkkokauppa/product/exceptions.py index 4477a9a16..4b6169cd2 100644 --- a/merchants/verkkokauppa/product/exceptions.py +++ b/tilavarauspalvelu/utils/verkkokauppa/product/exceptions.py @@ -1,4 +1,4 @@ -from merchants.verkkokauppa.exceptions import VerkkokauppaError +from tilavarauspalvelu.utils.verkkokauppa.exceptions import VerkkokauppaError class ProductError(VerkkokauppaError): diff --git a/merchants/verkkokauppa/product/types.py b/tilavarauspalvelu/utils/verkkokauppa/product/types.py similarity index 96% rename from merchants/verkkokauppa/product/types.py rename to tilavarauspalvelu/utils/verkkokauppa/product/types.py index 1ee38c09b..fd28b66b3 100644 --- a/merchants/verkkokauppa/product/types.py +++ b/tilavarauspalvelu/utils/verkkokauppa/product/types.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from typing import Any -from merchants.verkkokauppa.product.exceptions import ParseAccountingError, ParseProductError +from tilavarauspalvelu.utils.verkkokauppa.product.exceptions import ParseAccountingError, ParseProductError @dataclass(frozen=True) diff --git a/merchants/verkkokauppa/verkkokauppa_api_client.py b/tilavarauspalvelu/utils/verkkokauppa/verkkokauppa_api_client.py similarity index 93% rename from merchants/verkkokauppa/verkkokauppa_api_client.py rename to tilavarauspalvelu/utils/verkkokauppa/verkkokauppa_api_client.py index 17c45a83b..5a95aee6a 100644 --- a/merchants/verkkokauppa/verkkokauppa_api_client.py +++ b/tilavarauspalvelu/utils/verkkokauppa/verkkokauppa_api_client.py @@ -4,18 +4,28 @@ from django.conf import settings from requests import RequestException, Response -from merchants.verkkokauppa import constants as verkkokauppa_constants -from merchants.verkkokauppa.exceptions import VerkkokauppaConfigurationError -from merchants.verkkokauppa.merchants.exceptions import ( +from tilavarauspalvelu.utils.verkkokauppa import constants as verkkokauppa_constants +from tilavarauspalvelu.utils.verkkokauppa.exceptions import VerkkokauppaConfigurationError +from tilavarauspalvelu.utils.verkkokauppa.merchants.exceptions import ( CreateMerchantError, GetMerchantError, ParseMerchantError, UpdateMerchantError, ) -from merchants.verkkokauppa.merchants.types import CreateMerchantParams, Merchant, MerchantInfo, UpdateMerchantParams -from merchants.verkkokauppa.order.exceptions import CancelOrderError, CreateOrderError, GetOrderError, ParseOrderError -from merchants.verkkokauppa.order.types import CreateOrderParams, Order -from merchants.verkkokauppa.payment.exceptions import ( +from tilavarauspalvelu.utils.verkkokauppa.merchants.types import ( + CreateMerchantParams, + Merchant, + MerchantInfo, + UpdateMerchantParams, +) +from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import ( + CancelOrderError, + CreateOrderError, + GetOrderError, + ParseOrderError, +) +from tilavarauspalvelu.utils.verkkokauppa.order.types import CreateOrderParams, Order +from tilavarauspalvelu.utils.verkkokauppa.payment.exceptions import ( GetPaymentError, GetRefundStatusError, ParsePaymentError, @@ -23,14 +33,14 @@ ParseRefundStatusError, RefundPaymentError, ) -from merchants.verkkokauppa.payment.types import Payment, Refund, RefundStatusResult -from merchants.verkkokauppa.product.exceptions import ( +from tilavarauspalvelu.utils.verkkokauppa.payment.types import Payment, Refund, RefundStatusResult +from tilavarauspalvelu.utils.verkkokauppa.product.exceptions import ( CreateOrUpdateAccountingError, CreateProductError, ParseAccountingError, ParseProductError, ) -from merchants.verkkokauppa.product.types import ( +from tilavarauspalvelu.utils.verkkokauppa.product.types import ( Accounting, CreateOrUpdateAccountingParams, CreateProductParams, diff --git a/users/anonymisation.py b/users/anonymisation.py index d7a76ecab..7e67925bd 100644 --- a/users/anonymisation.py +++ b/users/anonymisation.py @@ -8,10 +8,10 @@ from applications.enums import ApplicationStatusChoice from applications.models import Address, Application, ApplicationSection, Person -from merchants.enums import OrderStatus from permissions.models import GeneralRole, UnitRole from reservations.enums import ReservationStateChoice, ReservationTypeChoice from reservations.models import Reservation +from tilavarauspalvelu.enums import OrderStatus from users.models import ReservationNotification, User ANONYMIZED = "Anonymized"