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/locale/fi/LC_MESSAGES/django.po b/locale/fi/LC_MESSAGES/django.po
index d5348ce48..819eff30c 100644
--- a/locale/fi/LC_MESSAGES/django.po
+++ b/locale/fi/LC_MESSAGES/django.po
@@ -39,7 +39,8 @@ msgstr ""
msgid "My bookings"
msgstr "Omat Varaukset"
-#: applications/admin/address.py merchants/admin/payment_merchant.py
+#: applications/admin/address.py
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
msgid "Street address"
msgstr "Katuosoite"
@@ -48,7 +49,7 @@ msgid "Post code"
msgstr "Postinumero"
#: applications/admin/address.py applications/models/city.py
-#: merchants/admin/payment_merchant.py
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
msgid "City"
msgstr "Kaupunki"
@@ -579,8 +580,9 @@ msgid "Reservation purpose"
msgstr "Varaustarkoitukset"
#: applications/admin/application_section/form.py
-#: merchants/admin/payment_order.py reservation_units/models/introduction.py
+#: reservation_units/models/introduction.py
#: reservation_units/models/reservation_unit_pricing.py
+#: tilavarauspalvelu/admin/payment_order/admin.py
msgid "Reservation unit"
msgstr "Varausyksikkö"
@@ -792,7 +794,8 @@ msgstr "Etunimi"
msgid "Last name"
msgstr "Sukunimi"
-#: applications/admin/person.py merchants/admin/payment_merchant.py
+#: applications/admin/person.py
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
msgid "Phone number"
msgstr "Puhelinnumero"
@@ -1331,15 +1334,15 @@ msgstr "SQL loki"
msgid "SQL logs"
msgstr "SQL lokit"
-#: config/settings.py merchants/enums.py
+#: config/settings.py tilavarauspalvelu/enums.py
msgid "Finnish"
msgstr "Suomi"
-#: config/settings.py merchants/enums.py
+#: config/settings.py tilavarauspalvelu/enums.py
msgid "English"
msgstr "Englanti"
-#: config/settings.py merchants/enums.py
+#: config/settings.py tilavarauspalvelu/enums.py
msgid "Swedish"
msgstr "Ruotsi"
@@ -1394,196 +1397,6 @@ msgstr "Koti"
msgid "Email Template Testing"
msgstr "Email-pohjan testaus"
-#: merchants/admin/payment_merchant.py
-msgid "The Paytrail Merchant ID should be a six-digit number."
-msgstr "Kaupan paytrail-tunnuksen tulisi olla kuusinumeroinen."
-
-#: merchants/admin/payment_merchant.py
-msgid "Shop ID"
-msgstr "Kaupan tunnus"
-
-#: merchants/admin/payment_merchant.py
-msgid "Business ID"
-msgstr "Y-tunnus"
-
-#: merchants/admin/payment_merchant.py
-msgid "ZIP code"
-msgstr "Postinumero"
-
-#: merchants/admin/payment_merchant.py
-msgid "Email address"
-msgstr "Sähköpostiosoite"
-
-#: merchants/admin/payment_merchant.py
-msgid "URL"
-msgstr "Verkko-osoite"
-
-#: merchants/admin/payment_merchant.py
-msgid "Terms of service URL"
-msgstr "Käyttöehtojen URL"
-
-#: merchants/admin/payment_merchant.py
-msgid "Merchant ID"
-msgstr "Kauppiastunniste"
-
-#: merchants/admin/payment_merchant.py
-msgid "Merchant name"
-msgstr "Kauppiaan nimi"
-
-#: merchants/admin/payment_merchant.py
-msgid "Value comes from the Merchant Experience API"
-msgstr "Arvo tulee kauppiaskokemusrajapinnasta"
-
-#: merchants/admin/payment_order.py
-msgid "Reservation"
-msgstr "Varaus"
-
-#: merchants/admin/payment_order.py
-msgid "Remote order ID"
-msgstr "Ulkoinen tilaustunniste"
-
-#: merchants/admin/payment_order.py
-msgid "Payment ID"
-msgstr "Maksutunniste"
-
-#: merchants/admin/payment_order.py
-msgid "Refund ID"
-msgstr "Maksun palautuksen tunniste"
-
-#: merchants/admin/payment_order.py
-msgid "Payment type"
-msgstr "Maksutyyppi"
-
-#: merchants/admin/payment_order.py
-msgid "Payment status"
-msgstr "Maksun tila"
-
-#: merchants/admin/payment_order.py
-msgid "Net amount"
-msgstr "Nettomäärä"
-
-#: merchants/admin/payment_order.py
-msgid "VAT amount"
-msgstr "ALV-määrä"
-
-#: merchants/admin/payment_order.py
-msgid "Total amount"
-msgstr "Kokonaismäärä"
-
-#: merchants/admin/payment_order.py
-msgid "Processed at"
-msgstr "Käsitelty"
-
-#: merchants/admin/payment_order.py
-msgid "Language"
-msgstr "Kieli"
-
-#: merchants/admin/payment_order.py
-msgid "Reservation user UUID"
-msgstr "Varaajan UUID"
-
-#: merchants/admin/payment_order.py
-msgid "Checkout URL"
-msgstr "Kassan URL"
-
-#: merchants/admin/payment_order.py
-msgid "Receipt URL"
-msgstr "Kuitin URL"
-
-#: merchants/admin/payment_order.py
-msgid "The reservation associated with this payment order"
-msgstr "Varaus, joka liittyy tähän maksutilaan"
-
-#: merchants/admin/payment_order.py
-msgid "eCommerce order ID"
-msgstr "eCommerce-tilaustunniste"
-
-#: merchants/admin/payment_order.py
-msgid "eCommerce payment ID"
-msgstr "eCommerce-maksutunniste"
-
-#: merchants/admin/payment_order.py
-msgid "Available only when order has been refunded"
-msgstr "Saatavilla vain, kun maksu on palautettu"
-
-#: merchants/admin/payment_order.py
-msgid ""
-"Search by Payment Order ID, Reservation ID, Remote Order ID, Reservation "
-"name, or Reservation Unit name"
-msgstr ""
-"Etsi maksutunnuksella, varauksen tunnuksella, verkkokaupan tunnuksella, "
-"varauksen nimellä tai varausyksikön nimellä"
-
-#: merchants/enums.py
-msgctxt "OrderStatus"
-msgid "Draft"
-msgstr "Luonnos"
-
-#: merchants/enums.py
-msgctxt "OrderStatus"
-msgid "Expired"
-msgstr "Rauennut"
-
-#: merchants/enums.py
-msgctxt "OrderStatus"
-msgid "Cancelled"
-msgstr "Peruttu"
-
-#: merchants/enums.py
-msgctxt "OrderStatus"
-msgid "Paid"
-msgstr "Maksettu"
-
-#: merchants/enums.py
-msgctxt "OrderStatus"
-msgid "Paid manually"
-msgstr "Maksettu manuaalisesti"
-
-#: merchants/enums.py
-msgctxt "OrderStatus"
-msgid "Refunded"
-msgstr "Palautettu"
-
-#: merchants/enums.py
-msgctxt "OrderStatus"
-msgid "Free"
-msgstr "Maksuton"
-
-#: merchants/enums.py reservation_units/enums.py
-msgctxt "PaymentType"
-msgid "On site"
-msgstr "Paikan päällä"
-
-#: merchants/enums.py reservation_units/enums.py
-msgctxt "PaymentType"
-msgid "Online"
-msgstr "Verkossa"
-
-#: merchants/enums.py reservation_units/enums.py
-msgctxt "PaymentType"
-msgid "Invoice"
-msgstr "Lasku"
-
-#: merchants/models/payment_accounting.py
-msgid ""
-"One of the following fields must be given: internal_order, profit_center, "
-"project"
-msgstr ""
-"Jokin seuraavista kentistä täytyy olla annettu: internal_order, "
-"profit_center, project"
-
-#: merchants/models/payment_order.py
-msgid "Must be greater than 0.01"
-msgstr "Täytyy olla suurempi kuin 0.01"
-
-#: merchants/models/payment_order.py
-msgid "Must be greater than 0"
-msgstr "Täytyy olla suurempi kuin 0"
-
-#: merchants/models/payment_order.py
-msgid "Must be the sum of net and vat amounts"
-msgstr "Täytyy olla netto- ja ALV-määrien summa"
-
#: opening_hours/admin/origin_hauki_resource.py
msgid ""
"OriginHaukiResources with this specific hash never have any opening hours."
@@ -2348,6 +2161,21 @@ msgctxt "PricingType"
msgid "Free"
msgstr "Maksuton"
+#: reservation_units/enums.py tilavarauspalvelu/enums.py
+msgctxt "PaymentType"
+msgid "Online"
+msgstr "Verkossa"
+
+#: reservation_units/enums.py tilavarauspalvelu/enums.py
+msgctxt "PaymentType"
+msgid "Invoice"
+msgstr "Lasku"
+
+#: reservation_units/enums.py tilavarauspalvelu/enums.py
+msgctxt "PaymentType"
+msgid "On site"
+msgstr "Paikan päällä"
+
#: reservation_units/enums.py
msgctxt "PriceUnit"
msgid "per 15 minutes"
@@ -3077,6 +2905,126 @@ msgstr "Ei, vie minut takaisin"
msgid "Reset allocations"
msgstr "Nollaa allokoinnit"
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
+msgid "The Paytrail Merchant ID should be a six-digit number."
+msgstr "Kaupan paytrail-tunnuksen tulisi olla kuusinumeroinen."
+
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
+msgid "Shop ID"
+msgstr "Kaupan tunnus"
+
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
+msgid "Business ID"
+msgstr "Y-tunnus"
+
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
+msgid "ZIP code"
+msgstr "Postinumero"
+
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
+msgid "Email address"
+msgstr "Sähköpostiosoite"
+
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
+msgid "URL"
+msgstr "Verkko-osoite"
+
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
+msgid "Terms of service URL"
+msgstr "Käyttöehtojen URL"
+
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
+msgid "Merchant ID"
+msgstr "Kauppiastunniste"
+
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
+msgid "Merchant name"
+msgstr "Kauppiaan nimi"
+
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
+msgid "Value comes from the Merchant Experience API"
+msgstr "Arvo tulee kauppiaskokemusrajapinnasta"
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Reservation"
+msgstr "Varaus"
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Remote order ID"
+msgstr "Ulkoinen tilaustunniste"
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Payment ID"
+msgstr "Maksutunniste"
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Refund ID"
+msgstr "Maksun palautuksen tunniste"
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Payment type"
+msgstr "Maksutyyppi"
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Payment status"
+msgstr "Maksun tila"
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Net amount"
+msgstr "Nettomäärä"
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "VAT amount"
+msgstr "ALV-määrä"
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Total amount"
+msgstr "Kokonaismäärä"
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Processed at"
+msgstr "Käsitelty"
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Language"
+msgstr "Kieli"
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Reservation user UUID"
+msgstr "Varaajan UUID"
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Checkout URL"
+msgstr "Kassan URL"
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Receipt URL"
+msgstr "Kuitin URL"
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "The reservation associated with this payment order"
+msgstr "Varaus, joka liittyy tähän maksutilaan"
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "eCommerce order ID"
+msgstr "eCommerce-tilaustunniste"
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "eCommerce payment ID"
+msgstr "eCommerce-maksutunniste"
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Available only when order has been refunded"
+msgstr "Saatavilla vain, kun maksu on palautettu"
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid ""
+"Search by Payment Order ID, Reservation ID, Remote Order ID, Reservation "
+"name, or Reservation Unit name"
+msgstr ""
+"Etsi maksutunnuksella, varauksen tunnuksella, verkkokaupan tunnuksella, "
+"varauksen nimellä tai varausyksikön nimellä"
+
#: tilavarauspalvelu/api/gdpr/views.py
msgid "User has upcoming or too recent reservations."
msgstr "Käyttäjällä on tulevia tai liian äskettäisiä varauksia."
@@ -3130,6 +3078,61 @@ msgstr "Kausivarauksen ehdot"
msgid "Service-specific terms"
msgstr "Palvelukohtaiset ehdot"
+#: tilavarauspalvelu/enums.py
+msgctxt "OrderStatus"
+msgid "Draft"
+msgstr "Luonnos"
+
+#: tilavarauspalvelu/enums.py
+msgctxt "OrderStatus"
+msgid "Expired"
+msgstr "Rauennut"
+
+#: tilavarauspalvelu/enums.py
+msgctxt "OrderStatus"
+msgid "Cancelled"
+msgstr "Peruttu"
+
+#: tilavarauspalvelu/enums.py
+msgctxt "OrderStatus"
+msgid "Paid"
+msgstr "Maksettu"
+
+#: tilavarauspalvelu/enums.py
+msgctxt "OrderStatus"
+msgid "Paid manually"
+msgstr "Maksettu manuaalisesti"
+
+#: tilavarauspalvelu/enums.py
+msgctxt "OrderStatus"
+msgid "Refunded"
+msgstr "Palautettu"
+
+#: tilavarauspalvelu/enums.py
+msgctxt "OrderStatus"
+msgid "Free"
+msgstr "Maksuton"
+
+#: tilavarauspalvelu/models/payment_accounting/model.py
+msgid ""
+"One of the following fields must be given: internal_order, profit_center, "
+"project"
+msgstr ""
+"Jokin seuraavista kentistä täytyy olla annettu: internal_order, "
+"profit_center, project"
+
+#: tilavarauspalvelu/models/payment_order/model.py
+msgid "Must be greater than 0.01"
+msgstr "Täytyy olla suurempi kuin 0.01"
+
+#: tilavarauspalvelu/models/payment_order/model.py
+msgid "Must be greater than 0"
+msgstr "Täytyy olla suurempi kuin 0"
+
+#: tilavarauspalvelu/models/payment_order/model.py
+msgid "Must be the sum of net and vat amounts"
+msgstr "Täytyy olla netto- ja ALV-määrien summa"
+
#: tilavarauspalvelu/models/terms_of_use/model.py
msgctxt "singular"
msgid "terms of use"
diff --git a/locale/sv/LC_MESSAGES/django.po b/locale/sv/LC_MESSAGES/django.po
index f1c755905..59abfc0a3 100644
--- a/locale/sv/LC_MESSAGES/django.po
+++ b/locale/sv/LC_MESSAGES/django.po
@@ -39,7 +39,8 @@ msgstr ""
msgid "My bookings"
msgstr "Mina bokningar"
-#: applications/admin/address.py merchants/admin/payment_merchant.py
+#: applications/admin/address.py
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
msgid "Street address"
msgstr ""
@@ -48,7 +49,7 @@ msgid "Post code"
msgstr ""
#: applications/admin/address.py applications/models/city.py
-#: merchants/admin/payment_merchant.py
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
msgid "City"
msgstr ""
@@ -562,8 +563,9 @@ msgid "Reservation purpose"
msgstr ""
#: applications/admin/application_section/form.py
-#: merchants/admin/payment_order.py reservation_units/models/introduction.py
+#: reservation_units/models/introduction.py
#: reservation_units/models/reservation_unit_pricing.py
+#: tilavarauspalvelu/admin/payment_order/admin.py
msgid "Reservation unit"
msgstr ""
@@ -765,7 +767,8 @@ msgstr ""
msgid "Last name"
msgstr ""
-#: applications/admin/person.py merchants/admin/payment_merchant.py
+#: applications/admin/person.py
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
msgid "Phone number"
msgstr ""
@@ -1291,15 +1294,15 @@ msgstr ""
msgid "SQL logs"
msgstr ""
-#: config/settings.py merchants/enums.py
+#: config/settings.py tilavarauspalvelu/enums.py
msgid "Finnish"
msgstr ""
-#: config/settings.py merchants/enums.py
+#: config/settings.py tilavarauspalvelu/enums.py
msgid "English"
msgstr ""
-#: config/settings.py merchants/enums.py
+#: config/settings.py tilavarauspalvelu/enums.py
msgid "Swedish"
msgstr ""
@@ -1350,192 +1353,6 @@ msgstr ""
msgid "Email Template Testing"
msgstr ""
-#: merchants/admin/payment_merchant.py
-msgid "The Paytrail Merchant ID should be a six-digit number."
-msgstr ""
-
-#: merchants/admin/payment_merchant.py
-msgid "Shop ID"
-msgstr ""
-
-#: merchants/admin/payment_merchant.py
-msgid "Business ID"
-msgstr ""
-
-#: merchants/admin/payment_merchant.py
-msgid "ZIP code"
-msgstr ""
-
-#: merchants/admin/payment_merchant.py
-msgid "Email address"
-msgstr ""
-
-#: merchants/admin/payment_merchant.py
-msgid "URL"
-msgstr ""
-
-#: merchants/admin/payment_merchant.py
-msgid "Terms of service URL"
-msgstr ""
-
-#: merchants/admin/payment_merchant.py
-msgid "Merchant ID"
-msgstr ""
-
-#: merchants/admin/payment_merchant.py
-msgid "Merchant name"
-msgstr ""
-
-#: merchants/admin/payment_merchant.py
-msgid "Value comes from the Merchant Experience API"
-msgstr ""
-
-#: merchants/admin/payment_order.py
-msgid "Reservation"
-msgstr ""
-
-#: merchants/admin/payment_order.py
-msgid "Remote order ID"
-msgstr ""
-
-#: merchants/admin/payment_order.py
-msgid "Payment ID"
-msgstr ""
-
-#: merchants/admin/payment_order.py
-msgid "Refund ID"
-msgstr ""
-
-#: merchants/admin/payment_order.py
-msgid "Payment type"
-msgstr ""
-
-#: merchants/admin/payment_order.py
-msgid "Payment status"
-msgstr ""
-
-#: merchants/admin/payment_order.py
-msgid "Net amount"
-msgstr ""
-
-#: merchants/admin/payment_order.py
-msgid "VAT amount"
-msgstr ""
-
-#: merchants/admin/payment_order.py
-msgid "Total amount"
-msgstr ""
-
-#: merchants/admin/payment_order.py
-msgid "Processed at"
-msgstr ""
-
-#: merchants/admin/payment_order.py
-msgid "Language"
-msgstr ""
-
-#: merchants/admin/payment_order.py
-msgid "Reservation user UUID"
-msgstr ""
-
-#: merchants/admin/payment_order.py
-msgid "Checkout URL"
-msgstr ""
-
-#: merchants/admin/payment_order.py
-msgid "Receipt URL"
-msgstr ""
-
-#: merchants/admin/payment_order.py
-msgid "The reservation associated with this payment order"
-msgstr ""
-
-#: merchants/admin/payment_order.py
-msgid "eCommerce order ID"
-msgstr ""
-
-#: merchants/admin/payment_order.py
-msgid "eCommerce payment ID"
-msgstr ""
-
-#: merchants/admin/payment_order.py
-msgid "Available only when order has been refunded"
-msgstr ""
-
-#: merchants/admin/payment_order.py
-msgid ""
-"Search by Payment Order ID, Reservation ID, Remote Order ID, Reservation "
-"name, or Reservation Unit name"
-msgstr ""
-
-#: merchants/enums.py
-msgctxt "OrderStatus"
-msgid "Draft"
-msgstr ""
-
-#: merchants/enums.py
-msgctxt "OrderStatus"
-msgid "Expired"
-msgstr ""
-
-#: merchants/enums.py
-msgctxt "OrderStatus"
-msgid "Cancelled"
-msgstr ""
-
-#: merchants/enums.py
-msgctxt "OrderStatus"
-msgid "Paid"
-msgstr ""
-
-#: merchants/enums.py
-msgctxt "OrderStatus"
-msgid "Paid manually"
-msgstr ""
-
-#: merchants/enums.py
-msgctxt "OrderStatus"
-msgid "Refunded"
-msgstr ""
-
-#: merchants/enums.py
-msgctxt "OrderStatus"
-msgid "Free"
-msgstr ""
-
-#: merchants/enums.py reservation_units/enums.py
-msgctxt "PaymentType"
-msgid "On site"
-msgstr ""
-
-#: merchants/enums.py reservation_units/enums.py
-msgctxt "PaymentType"
-msgid "Online"
-msgstr ""
-
-#: merchants/enums.py reservation_units/enums.py
-msgctxt "PaymentType"
-msgid "Invoice"
-msgstr ""
-
-#: merchants/models/payment_accounting.py
-msgid ""
-"One of the following fields must be given: internal_order, profit_center, "
-"project"
-msgstr ""
-
-#: merchants/models/payment_order.py
-msgid "Must be greater than 0.01"
-msgstr ""
-
-#: merchants/models/payment_order.py
-msgid "Must be greater than 0"
-msgstr ""
-
-#: merchants/models/payment_order.py
-msgid "Must be the sum of net and vat amounts"
-msgstr ""
-
#: opening_hours/admin/origin_hauki_resource.py
msgid ""
"OriginHaukiResources with this specific hash never have any opening hours."
@@ -2278,6 +2095,21 @@ msgctxt "PricingType"
msgid "Free"
msgstr ""
+#: reservation_units/enums.py tilavarauspalvelu/enums.py
+msgctxt "PaymentType"
+msgid "Online"
+msgstr ""
+
+#: reservation_units/enums.py tilavarauspalvelu/enums.py
+msgctxt "PaymentType"
+msgid "Invoice"
+msgstr ""
+
+#: reservation_units/enums.py tilavarauspalvelu/enums.py
+msgctxt "PaymentType"
+msgid "On site"
+msgstr ""
+
#: reservation_units/enums.py
msgctxt "PriceUnit"
msgid "per 15 minutes"
@@ -3007,6 +2839,124 @@ msgstr ""
msgid "Reset allocations"
msgstr ""
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
+msgid "The Paytrail Merchant ID should be a six-digit number."
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
+msgid "Shop ID"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
+msgid "Business ID"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
+msgid "ZIP code"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
+msgid "Email address"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
+msgid "URL"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
+msgid "Terms of service URL"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
+msgid "Merchant ID"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
+msgid "Merchant name"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_merchant/admin.py
+msgid "Value comes from the Merchant Experience API"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Reservation"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Remote order ID"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Payment ID"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Refund ID"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Payment type"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Payment status"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Net amount"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "VAT amount"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Total amount"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Processed at"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Language"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Reservation user UUID"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Checkout URL"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Receipt URL"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "The reservation associated with this payment order"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "eCommerce order ID"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "eCommerce payment ID"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid "Available only when order has been refunded"
+msgstr ""
+
+#: tilavarauspalvelu/admin/payment_order/admin.py
+msgid ""
+"Search by Payment Order ID, Reservation ID, Remote Order ID, Reservation "
+"name, or Reservation Unit name"
+msgstr ""
+
#: tilavarauspalvelu/api/gdpr/views.py
msgid "User has upcoming or too recent reservations."
msgstr ""
@@ -3058,6 +3008,59 @@ msgstr ""
msgid "Service-specific terms"
msgstr ""
+#: tilavarauspalvelu/enums.py
+msgctxt "OrderStatus"
+msgid "Draft"
+msgstr ""
+
+#: tilavarauspalvelu/enums.py
+msgctxt "OrderStatus"
+msgid "Expired"
+msgstr ""
+
+#: tilavarauspalvelu/enums.py
+msgctxt "OrderStatus"
+msgid "Cancelled"
+msgstr ""
+
+#: tilavarauspalvelu/enums.py
+msgctxt "OrderStatus"
+msgid "Paid"
+msgstr ""
+
+#: tilavarauspalvelu/enums.py
+msgctxt "OrderStatus"
+msgid "Paid manually"
+msgstr ""
+
+#: tilavarauspalvelu/enums.py
+msgctxt "OrderStatus"
+msgid "Refunded"
+msgstr ""
+
+#: tilavarauspalvelu/enums.py
+msgctxt "OrderStatus"
+msgid "Free"
+msgstr ""
+
+#: tilavarauspalvelu/models/payment_accounting/model.py
+msgid ""
+"One of the following fields must be given: internal_order, profit_center, "
+"project"
+msgstr ""
+
+#: tilavarauspalvelu/models/payment_order/model.py
+msgid "Must be greater than 0.01"
+msgstr ""
+
+#: tilavarauspalvelu/models/payment_order/model.py
+msgid "Must be greater than 0"
+msgstr ""
+
+#: tilavarauspalvelu/models/payment_order/model.py
+msgid "Must be the sum of net and vat amounts"
+msgstr ""
+
#: tilavarauspalvelu/models/terms_of_use/model.py
msgctxt "singular"
msgid "terms of use"
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 298f63acf..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.db import models
-from django.utils.translation import gettext_lazy as _
-
-from common.typing import WSGIRequest
-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 2f8086d89..958e8667f 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 common.typing import WSGIRequest
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_gdpr_api/test_gdpr_api.py b/tests/test_gdpr_api/test_gdpr_api.py
index ec245d5a3..58aa0baba 100644
--- a/tests/test_gdpr_api/test_gdpr_api.py
+++ b/tests/test_gdpr_api/test_gdpr_api.py
@@ -7,9 +7,9 @@
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 tilavarauspalvelu.enums import OrderStatus
from .helpers import get_gdpr_auth_header, patch_oidc_config
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_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..65dc5ed65 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_order.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_order/admin.py b/tilavarauspalvelu/admin/payment_order/admin.py
index e69de29bb..eefc9f733 100644
--- a/tilavarauspalvelu/admin/payment_order/admin.py
+++ b/tilavarauspalvelu/admin/payment_order/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/admin/payment_product/__init__.py b/tilavarauspalvelu/admin/payment_product/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/tilavarauspalvelu/admin/payment_product/admin.py b/tilavarauspalvelu/admin/payment_product/admin.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/tilavarauspalvelu/api/graphql/schema.py b/tilavarauspalvelu/api/graphql/schema.py
index d1588bc5b..ed92637ed 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 ab2d612c4..b29a818dc 100644
--- a/merchants/mock_verkkokauppa_api/views.py
+++ b/tilavarauspalvelu/api/mock_verkkokauppa_api/views.py
@@ -9,14 +9,19 @@
from common.date_utils import local_datetime
from common.typing import WSGIRequest
-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"