Migrate to global currency system in commerce app

Removed per-product currency in favor of a global site currency managed via SiteConfiguration. Updated models, views, templates, and Stripe integration to use the global currency. Added migration, management command for migration, and API endpoint for currency info. Improved permissions and filtering for orders, reviews, and carts. Expanded supported currencies in configuration.
This commit is contained in:
2026-01-24 21:51:56 +01:00
parent 8f6d864b4b
commit 775709bd08
19 changed files with 371 additions and 102 deletions

10
.idea/.gitignore generated vendored Normal file
View File

@@ -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

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

4
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (vontor-cz)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/vontor-cz.iml" filepath="$PROJECT_DIR$/.idea/vontor-cz.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

35
.idea/vontor-cz.iml generated Normal file
View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="FacetManager">
<facet type="django" name="Django">
<configuration>
<option name="rootFolder" value="$MODULE_DIR$/backend" />
<option name="settingsModule" value="vontor_cz/settings.py" />
<option name="manageScript" value="$MODULE_DIR$/backend/manage.py" />
<option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="migrations" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/backend" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.13 (vontor-cz)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="GOOGLE" />
<option name="myDocStringFormat" value="Google" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/backend/account/templates" />
</list>
</option>
</component>
</module>

View File

@@ -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()

View File

@@ -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,
}
)

View File

@@ -60,7 +60,7 @@
{% if product.price %}
<div class="product-price">
{{ product.price|floatformat:0 }} {{ product.currency|default:"" }}
{{ product.price|floatformat:0 }} {{ site_currency|default:"" }}
</div>
{% endif %}

View File

@@ -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': '',
'USD': '$',
'GBP': '£',
'PLN': '',
'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
]
})

View File

@@ -0,0 +1 @@
# Management commands module

View File

@@ -0,0 +1 @@
# Commerce management commands

View File

@@ -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!")
)

View File

@@ -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
),
),
]

View File

@@ -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(
@@ -258,12 +260,24 @@ class Order(models.Model):
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)

View File

@@ -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'),
]

View File

@@ -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<product_id>[^/.]+)')
@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:

View File

@@ -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"

View File

@@ -20,6 +20,10 @@ 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",
@@ -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,
}],