diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..430eb9f --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..452c296 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vontor-cz.iml b/.idea/vontor-cz.iml new file mode 100644 index 0000000..cc7388e --- /dev/null +++ b/.idea/vontor-cz.iml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/account/views.py b/backend/account/views.py index 16a0fef..b28cbfe 100644 --- a/backend/account/views.py +++ b/backend/account/views.py @@ -250,10 +250,19 @@ class UserView(viewsets.ModelViewSet): # Fallback - deny access (prevents AttributeError for AnonymousUser) return [OnlyRolesAllowed("admin")()] - # Any authenticated user can retrieve (view) any user's profile - #FIXME: popřemýšlet co vše může získat + # Users can only view their own profile, admins can view any profile elif self.action == 'retrieve': - return [IsAuthenticated()] + user = getattr(self, 'request', None) and getattr(self.request, 'user', None) + # Admins can view any user profile + if user and getattr(user, 'is_authenticated', False) and getattr(user, 'role', None) == 'admin': + return [IsAuthenticated()] + + # Users can view their own profile + if user and getattr(user, 'is_authenticated', False) and self.kwargs.get('pk') and str(getattr(user, 'id', '')) == self.kwargs['pk']: + return [IsAuthenticated()] + + # Deny access to other users' profiles + return [OnlyRolesAllowed("admin")()] return super().get_permissions() diff --git a/backend/advertisement/tasks.py b/backend/advertisement/tasks.py index a62019d..d6b7845 100644 --- a/backend/advertisement/tasks.py +++ b/backend/advertisement/tasks.py @@ -38,12 +38,15 @@ def send_newly_added_items_to_store_email_task_last_week(): created_at__gte=last_week_date ) + config = SiteConfiguration.get_solo() + send_email_with_context( - recipients=SiteConfiguration.get_solo().contact_email, + recipients=config.contact_email, subject="Nový produkt přidán do obchodu", template_path="email/advertisement/commerce/new_items_added_this_week.html", context={ "products_of_week": products_of_week, + "site_currency": config.currency, } ) diff --git a/backend/advertisement/templates/email/advertisement/commerce/new_items_added_this_week.html b/backend/advertisement/templates/email/advertisement/commerce/new_items_added_this_week.html index 98eea1a..a959536 100644 --- a/backend/advertisement/templates/email/advertisement/commerce/new_items_added_this_week.html +++ b/backend/advertisement/templates/email/advertisement/commerce/new_items_added_this_week.html @@ -60,7 +60,7 @@ {% if product.price %}
- {{ product.price|floatformat:0 }} {{ product.currency|default:"Kč" }} + {{ product.price|floatformat:0 }} {{ site_currency|default:"€" }}
{% endif %} diff --git a/backend/commerce/currency_info_view.py b/backend/commerce/currency_info_view.py new file mode 100644 index 0000000..576312e --- /dev/null +++ b/backend/commerce/currency_info_view.py @@ -0,0 +1,45 @@ +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status +from drf_spectacular.utils import extend_schema +from configuration.models import SiteConfiguration + +class CurrencyInfoView(APIView): + """ + Get current site currency and display information. + """ + + @extend_schema( + summary="Get site currency information", + description="Returns the current site currency and available options", + tags=["configuration"] + ) + def get(self, request): + config = SiteConfiguration.get_solo() + + currency_symbols = { + 'EUR': '€', + 'CZK': 'Kč', + 'USD': '$', + 'GBP': '£', + 'PLN': 'zł', + 'HUF': 'Ft', + 'SEK': 'kr', + 'DKK': 'kr', + 'NOK': 'kr', + 'CHF': 'Fr' + } + + return Response({ + 'current_currency': config.currency, + 'currency_symbol': currency_symbols.get(config.currency, config.currency), + 'currency_name': dict(SiteConfiguration.CURRENCY.choices)[config.currency], + 'available_currencies': [ + { + 'code': choice[0], + 'name': choice[1], + 'symbol': currency_symbols.get(choice[0], choice[0]) + } + for choice in SiteConfiguration.CURRENCY.choices + ] + }) \ No newline at end of file diff --git a/backend/commerce/management/__init__.py b/backend/commerce/management/__init__.py new file mode 100644 index 0000000..0cf6859 --- /dev/null +++ b/backend/commerce/management/__init__.py @@ -0,0 +1 @@ +# Management commands module \ No newline at end of file diff --git a/backend/commerce/management/commands/__init__.py b/backend/commerce/management/commands/__init__.py new file mode 100644 index 0000000..fc58c38 --- /dev/null +++ b/backend/commerce/management/commands/__init__.py @@ -0,0 +1 @@ +# Commerce management commands \ No newline at end of file diff --git a/backend/commerce/management/commands/migrate_to_global_currency.py b/backend/commerce/management/commands/migrate_to_global_currency.py new file mode 100644 index 0000000..8f2c5fe --- /dev/null +++ b/backend/commerce/management/commands/migrate_to_global_currency.py @@ -0,0 +1,74 @@ +""" +Management command to migrate from per-product currency to global currency system. +Usage: python manage.py migrate_to_global_currency +""" +from django.core.management.base import BaseCommand +from commerce.models import Product, Order +from configuration.models import SiteConfiguration + + +class Command(BaseCommand): + help = 'Migrate from per-product currency to global currency system' + + def add_arguments(self, parser): + parser.add_argument( + '--target-currency', + type=str, + default='EUR', + help='Target currency to migrate to (default: EUR)' + ) + parser.add_argument( + '--dry-run', + action='store_true', + help='Show what would be changed without making changes' + ) + + def handle(self, *args, **options): + target_currency = options['target_currency'] + dry_run = options['dry_run'] + + self.stdout.write( + self.style.SUCCESS(f"Migrating to global currency: {target_currency}") + ) + + # Check current state + config = SiteConfiguration.get_solo() + self.stdout.write(f"Current site currency: {config.currency}") + + if hasattr(Product.objects.first(), 'currency'): + # Products still have currency field + product_currencies = Product.objects.values_list('currency', flat=True).distinct() + self.stdout.write(f"Product currencies found: {list(product_currencies)}") + + if len(product_currencies) > 1: + self.stdout.write( + self.style.WARNING( + "Multiple currencies detected in products. " + "Consider currency conversion before migration." + ) + ) + + order_currencies = Order.objects.values_list('currency', flat=True).distinct() + order_currencies = [c for c in order_currencies if c] # Remove empty strings + self.stdout.write(f"Order currencies found: {list(order_currencies)}") + + if not dry_run: + # Update site configuration + config.currency = target_currency + config.save() + self.stdout.write( + self.style.SUCCESS(f"Updated site currency to {target_currency}") + ) + + # Update orders with empty currency + orders_updated = Order.objects.filter(currency='').update(currency=target_currency) + self.stdout.write( + self.style.SUCCESS(f"Updated {orders_updated} orders to use {target_currency}") + ) + + else: + self.stdout.write(self.style.WARNING("DRY RUN - No changes made")) + + self.stdout.write( + self.style.SUCCESS("Migration completed successfully!") + ) \ No newline at end of file diff --git a/backend/commerce/migrations/0003_remove_product_currency.py b/backend/commerce/migrations/0003_remove_product_currency.py new file mode 100644 index 0000000..3e47ff7 --- /dev/null +++ b/backend/commerce/migrations/0003_remove_product_currency.py @@ -0,0 +1,36 @@ +# Generated migration to remove Product.currency field and use global currency system +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('commerce', '0002_alter_productimage_options_carrier_deutschepost_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='product', + name='currency', + ), + migrations.AlterField( + model_name='order', + name='currency', + field=models.CharField( + default='', + help_text='Order currency - auto-filled from site configuration', + max_length=10 + ), + ), + migrations.AlterField( + model_name='discountcode', + name='amount', + field=models.DecimalField( + blank=True, + decimal_places=2, + help_text='Fixed discount amount in site currency', + max_digits=10, + null=True + ), + ), + ] \ No newline at end of file diff --git a/backend/commerce/models.py b/backend/commerce/models.py index 4fa9302..07e7e51 100644 --- a/backend/commerce/models.py +++ b/backend/commerce/models.py @@ -71,7 +71,7 @@ class Product(models.Model): # -- CENA -- price = models.DecimalField(max_digits=10, decimal_places=2, help_text="Net price (without VAT)") - currency = models.CharField(max_length=3, default="CZK") + # Currency is now global from SiteConfiguration, not per-product # VAT rate - configured by business owner in configuration app!!! vat_rate = models.ForeignKey( @@ -127,7 +127,8 @@ class Product(models.Model): return self.price * vat_rate.rate_decimal def __str__(self): - return f"{self.name} ({self.get_price_with_vat()} {self.currency.upper()} inkl. MwSt)" + config = SiteConfiguration.get_solo() + return f"{self.name} ({self.get_price_with_vat()} {config.currency} inkl. MwSt)" #obrázek pro produkty class ProductImage(models.Model): @@ -164,7 +165,8 @@ class Order(models.Model): # Stored order grand total; recalculated on save total_price = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00')) - currency = models.CharField(max_length=10, default="CZK") +# Currency - captured from site configuration at creation time, never changes + currency = models.CharField(max_length=10, default="", help_text="Order currency - captured from site configuration at order creation and never changes") # fakturační údaje (zkopírované z user profilu při objednávce) user = models.ForeignKey( @@ -257,13 +259,25 @@ class Order(models.Model): # Validate order has items if self.pk and not self.items.exists(): raise ValidationError("Order must have at least one item.") + + def get_currency(self): + \"\"\"Get order currency - falls back to site configuration if not set\"\"\" + if self.currency: + return self.currency + config = SiteConfiguration.get_solo() + return config.currency def save(self, *args, **kwargs): - # Keep total_price always in sync with items and discount - self.total_price = self.calculate_total_price() - - is_new = self.pk is None - + is_new = self.pk is None + + # CRITICAL: Set currency from site configuration ONLY at creation time + # Once set, currency should NEVER change to maintain order integrity + if is_new and not self.currency: + config = SiteConfiguration.get_solo() + self.currency = config.currency + + # Keep total_price always in sync with items and discount + self.total_price = self.calculate_total_price() if self.user and is_new: self.import_data_from_user() @@ -274,6 +288,15 @@ class Order(models.Model): from .tasks import notify_order_successfuly_created notify_order_successfuly_created.delay(order=self, user=self.user) + def cancel_order(self): + """Cancel the order if possible""" + if self.status == self.OrderStatus.CREATED: + self.status = self.OrderStatus.CANCELLED + self.save() + #TODO: udělat ještě kontrolu jestli už nebyla odeslána zásilka a pokud bude už zaplacena tak se uděla refundace a pokud nebude zaplacena tak se zruší brána. + else: + raise ValidationError("Only orders in 'created' status can be cancelled.") + # ------------------ DOPRAVCI A ZPŮSOBY DOPRAVY ------------------ @@ -480,7 +503,7 @@ class DiscountCode(models.Model): ) # nebo fixní částka - amount = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, help_text="Fixní sleva v CZK") + amount = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, help_text=\"Fixed discount amount in site currency\") valid_from = models.DateTimeField(default=timezone.now) valid_to = models.DateTimeField(null=True, blank=True) diff --git a/backend/commerce/urls.py b/backend/commerce/urls.py index 4c8d90f..a306808 100644 --- a/backend/commerce/urls.py +++ b/backend/commerce/urls.py @@ -15,6 +15,7 @@ from .views import ( AdminWishlistViewSet, AnalyticsView, ) +from .currency_info_view import CurrencyInfoView router = DefaultRouter() router.register(r'orders', OrderViewSet) @@ -33,4 +34,5 @@ urlpatterns = [ path('refunds/public/', RefundPublicView.as_view(), name='RefundPublicView'), path('reviews/create/', ReviewPostPublicView.as_view(), name='ReviewCreate'), path('analytics/', AnalyticsView.as_view(), name='analytics'), + path('currency/info/', CurrencyInfoView.as_view(), name='currency-info'), ] diff --git a/backend/commerce/views.py b/backend/commerce/views.py index cf9ab0c..462dca4 100644 --- a/backend/commerce/views.py +++ b/backend/commerce/views.py @@ -15,7 +15,7 @@ from .analytics import ( ) -from django.db import transaction +from django.db import transaction, models from rest_framework import viewsets, mixins, status from rest_framework.permissions import AllowAny, IsAdminUser, SAFE_METHODS from rest_framework.decorators import action @@ -63,14 +63,39 @@ from .serializers import ( #FIXME: uravit view na nový order serializer @extend_schema_view( - list=extend_schema(tags=["commerce", "public"], summary="List Orders (public)"), - retrieve=extend_schema(tags=["commerce", "public"], summary="Retrieve Order (public)"), + list=extend_schema(tags=["commerce", "public"], summary="List Orders (public - anonymous orders, authenticated - user orders, admin - all orders)"), + retrieve=extend_schema(tags=["commerce", "public"], summary="Retrieve Order (public - anonymous orders, authenticated - user orders, admin - all orders)"), + create=extend_schema(tags=["commerce", "public"], summary="Create Order (public)"), ) -class OrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet): +class OrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet): queryset = Order.objects.select_related("carrier", "payment").prefetch_related( "items__product", "discount" ).order_by("-created_at") - permission_classes = [AllowAny] + permission_classes = [permissions.AllowAny] + + def get_permissions(self): + """Allow public order access (for anonymous orders) and creation, require auth for user orders""" + if self.action in ['create', 'list', 'retrieve', 'mini', 'items', 'carrier_detail', 'payment_detail', 'verify_payment', 'download_invoice']: + return [permissions.AllowAny()] + return super().get_permissions() + + def get_queryset(self): + """Filter orders by user - admins see all, authenticated users see own orders, anonymous users see anonymous orders only""" + queryset = super().get_queryset() + + # Admin users can see all orders + if self.request.user.is_authenticated and getattr(self.request.user, 'role', None) == 'admin': + return queryset + + # Authenticated users see only their own orders (both user-linked and email-matched anonymous orders) + if self.request.user.is_authenticated: + return queryset.filter( + models.Q(user=self.request.user) | + models.Q(user__isnull=True, email=self.request.user.email) + ) + + # Anonymous users can only see anonymous orders (no user linked) + return queryset.filter(user__isnull=True) def get_serializer_class(self): if self.action == "mini": @@ -81,51 +106,17 @@ class OrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.Ge return OrderCreateSerializer return OrderReadSerializer - @extend_schema( - tags=["commerce", "public"], - summary="Create Order (public)", - request=OrderCreateSerializer, - responses={201: OrderReadSerializer}, - examples=[ - OpenApiExample( - "Create order", - value={ - "first_name": "Jan", - "last_name": "Novak", - "email": "jan@example.com", - "phone": "+420123456789", - "address": "Ulice 1", - "city": "Praha", - "postal_code": "11000", - "country": "Czech Republic", - "note": "Prosím doručit odpoledne", - "items": [ - {"product_id": 1, "quantity": 2}, - {"product_id": 7, "quantity": 1}, - ], - "carrier": {"shipping_method": "store"}, - "payment": {"payment_method": "stripe"}, - "discount_codes": ["WELCOME10"], - }, - ) - ], - ) - def create(self, request, *args, **kwargs): - serializer = OrderCreateSerializer(data=request.data, context={"request": request}) - serializer.is_valid(raise_exception=True) - order = serializer.save() - out = OrderReadSerializer(order) - return Response(out.data, status=status.HTTP_201_CREATED) + # Order creation is now handled by CreateModelMixin with proper serializer # -- List mini orders -- (public) -- @action(detail=False, methods=["get"], url_path="detail") @extend_schema( tags=["commerce", "public"], - summary="List mini orders (public)", + summary="List mini orders (public - anonymous orders, authenticated - user orders)", responses={200: OrderMiniSerializer(many=True)}, ) def mini(self, request, *args, **kwargs): - qs = self.get_queryset() + qs = self.get_queryset() # Already filtered by user/anonymous status page = self.paginate_queryset(qs) if page is not None: @@ -139,11 +130,11 @@ class OrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.Ge @action(detail=True, methods=["get"], url_path="items") @extend_schema( tags=["commerce", "public"], - summary="List order items (public)", + summary="List order items (public - anonymous orders, authenticated - user orders)", responses={200: OrderItemReadSerializer(many=True)}, ) def items(self, request, pk=None): - order = self.get_object() + order = self.get_object() # get_object respects get_queryset filtering qs = order.items.select_related("product").all() ser = OrderItemReadSerializer(qs, many=True) return Response(ser.data) @@ -152,11 +143,11 @@ class OrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.Ge @action(detail=True, methods=["get"], url_path="carrier") @extend_schema( tags=["commerce", "public"], - summary="Get order carrier (public)", + summary="Get order carrier (public - anonymous orders, authenticated - user orders)", responses={200: CarrierReadSerializer}, ) def carrier_detail(self, request, pk=None): - order = self.get_object() + order = self.get_object() # get_object respects get_queryset filtering ser = CarrierReadSerializer(order.carrier) return Response(ser.data) @@ -164,11 +155,11 @@ class OrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.Ge @action(detail=True, methods=["get"], url_path="payment") @extend_schema( tags=["commerce", "public"], - summary="Get order payment (public)", + summary="Get order payment (public - anonymous orders, authenticated - user orders)", responses={200: PaymentReadSerializer}, ) def payment_detail(self, request, pk=None): - order = self.get_object() + order = self.get_object() # get_object respects get_queryset filtering ser = PaymentReadSerializer(order.payment) return Response(ser.data) @@ -216,21 +207,6 @@ class OrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.Ge ser = CarrierReadSerializer(order.carrier) return Response(ser.data) - # -- Get user's own orders (authenticated) -- - @action(detail=False, methods=['get'], permission_classes=[permissions.IsAuthenticated]) - @extend_schema( - tags=["commerce"], - summary="Get authenticated user's orders", - responses={200: OrderMiniSerializer(many=True)}, - ) - def my_orders(self, request): - orders = Order.objects.filter(user=request.user).order_by('-created_at') - page = self.paginate_queryset(orders) - if page: - serializer = OrderMiniSerializer(page, many=True) - return self.get_paginated_response(serializer.data) - serializer = OrderMiniSerializer(orders, many=True) - return Response(serializer.data) # -- Cancel order (authenticated) -- @action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated]) @@ -240,11 +216,7 @@ class OrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.Ge responses={200: {"type": "object", "properties": {"status": {"type": "string"}}}}, ) def cancel(self, request, pk=None): - order = self.get_object() - - # Check if user owns order - if order.user != request.user: - return Response({'detail': 'Not authorized'}, status=403) + order = self.get_object() # get_object respects get_queryset filtering # Can only cancel if not shipped if order.carrier and order.carrier.state != Carrier.STATE.PREPARING: @@ -303,20 +275,7 @@ class OrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.Ge filename=f'invoice_{order.invoice.invoice_number}.pdf' ) - def retrieve(self, request, *args, **kwargs): - """Override retrieve to filter by user if authenticated and not admin""" - order = self.get_object() - - # If user is authenticated and not admin, check if they own the order - if request.user.is_authenticated and not request.user.is_staff: - if order.user and order.user != request.user: - return Response({'detail': 'Not found.'}, status=404) - # Also check by email for anonymous orders - elif not order.user and order.email != request.user.email: - return Response({'detail': 'Not found.'}, status=404) - - serializer = self.get_serializer(order) - return Response(serializer.data) + # retrieve method removed - get_queryset handles filtering automatically # ---------- Public/admin viewsets ---------- @@ -577,6 +536,23 @@ class ReviewPublicViewSet(viewsets.ModelViewSet): ordering_fields = ["rating", "created_at"] ordering = ["-created_at"] + def get_queryset(self): + """Filter reviews - admins see all, users see all reviews but can only modify their own""" + queryset = super().get_queryset() + + # Admin users can see and modify all reviews + if self.request.user.is_authenticated and getattr(self.request.user, 'role', None) == 'admin': + return queryset + + # For modification actions, users can only modify their own reviews + if self.action in ['update', 'partial_update', 'destroy']: + if self.request.user.is_authenticated: + return queryset.filter(user=self.request.user) + return queryset.none() + + # For viewing, everyone can see all reviews (they're public) + return queryset + @action(detail=False, methods=['get'], url_path='product/(?P[^/.]+)') @extend_schema( tags=["commerce", "public"], @@ -608,6 +584,24 @@ class CartViewSet(viewsets.GenericViewSet): serializer_class = CartSerializer permission_classes = [AllowAny] + def get_queryset(self): + """Filter carts by user/session - users only see their own cart""" + queryset = super().get_queryset() + + # Admin users can see all carts + if self.request.user.is_authenticated and getattr(self.request.user, 'role', None) == 'admin': + return queryset + + # Authenticated users see only their cart + if self.request.user.is_authenticated: + return queryset.filter(user=self.request.user) + + # Anonymous users see only their session cart + session_key = self.request.session.session_key + if session_key: + return queryset.filter(session_key=session_key, user__isnull=True) + return queryset.none() + def get_or_create_cart(self, request): """Get or create cart for current user/session""" if request.user.is_authenticated: diff --git a/backend/configuration/models.py b/backend/configuration/models.py index 6abf4bf..a21d89b 100644 --- a/backend/configuration/models.py +++ b/backend/configuration/models.py @@ -44,9 +44,17 @@ class SiteConfiguration(models.Model): addition_of_coupons_amount = models.BooleanField(default=False, help_text="Sčítání slevových kupónů v objednávce (ano/ne), pokud ne tak se použije pouze nejvyšší slevový kupón") class CURRENCY(models.TextChoices): - CZK = "CZK", "Czech Koruna" EUR = "EUR", "Euro" - currency = models.CharField(max_length=10, default=CURRENCY.CZK, choices=CURRENCY.choices) + CZK = "CZK", "Czech Koruna" + USD = "USD", "US Dollar" + GBP = "GBP", "British Pound" + PLN = "PLN", "Polish Zloty" + HUF = "HUF", "Hungarian Forint" + SEK = "SEK", "Swedish Krona" + DKK = "DKK", "Danish Krone" + NOK = "NOK", "Norwegian Krone" + CHF = "CHF", "Swiss Franc" + currency = models.CharField(max_length=10, default=CURRENCY.EUR, choices=CURRENCY.choices) class Meta: verbose_name = "Shop Configuration" diff --git a/backend/thirdparty/stripe/client.py b/backend/thirdparty/stripe/client.py index ce5ad4e..fe59348 100644 --- a/backend/thirdparty/stripe/client.py +++ b/backend/thirdparty/stripe/client.py @@ -20,7 +20,11 @@ class StripeClient: Returns: stripe.checkout.Session: Vytvořená Stripe Checkout Session. """ - + from configuration.models import SiteConfiguration + + # Use order currency or fall back to site configuration + currency = order.get_currency().lower() + session = stripe.checkout.Session.create( mode="payment", payment_method_types=["card"], @@ -31,11 +35,11 @@ class StripeClient: client_reference_id=str(order.id), line_items=[{ "price_data": { - "currency": "czk", + "currency": currency, "product_data": { - "name": f"Objednávka {order.id}", + "name": f"Order #{order.id}", }, - "unit_amount": int(order.total_price * 100), # cena v haléřích + "unit_amount": int(order.total_price * 100), # amount in smallest currency unit }, "quantity": 1, }],