diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..2ba986f
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,15 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "chrome",
+ "request": "launch",
+ "name": "Launch Chrome against localhost",
+ "url": "http://localhost:8080",
+ "webRoot": "${workspaceFolder}"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/backend/account/models.py b/backend/account/models.py
index ef7d280..226f3a2 100644
--- a/backend/account/models.py
+++ b/backend/account/models.py
@@ -16,15 +16,6 @@ import logging
logger = logging.getLogger(__name__)
-# Custom User Manager to handle soft deletion
-class CustomUserActiveManager(UserManager):
- def get_queryset(self):
- return super().get_queryset().filter(is_deleted=False)
-
-# Custom User Manager to handle all users, including soft deleted
-class CustomUserAllManager(UserManager):
- def get_queryset(self):
- return super().get_queryset()
class CustomUser(SoftDeleteModel, AbstractUser):
@@ -67,33 +58,6 @@ class CustomUser(SoftDeleteModel, AbstractUser):
email = models.EmailField(unique=True, db_index=True)
create_time = models.DateTimeField(auto_now_add=True)
-
- """company_id = models.CharField(
- max_length=8,
- blank=True,
- null=True,
- validators=[
- RegexValidator(
- regex=r'^\d{8}$',
- message="Company ID must contain exactly 8 digits.",
- code='invalid_company_id'
- )
- ]
- )"""
-
- """personal_id = models.CharField(
- max_length=11,
- blank=True,
- null=True,
- validators=[
- RegexValidator(
- regex=r'^\d{6}/\d{3,4}$',
- message="Personal ID must be in the format 123456/7890.",
- code='invalid_personal_id'
- )
- ]
- )"""
-
city = models.CharField(null=True, blank=True, max_length=100)
street = models.CharField(null=True, blank=True, max_length=200)
@@ -113,9 +77,6 @@ class CustomUser(SoftDeleteModel, AbstractUser):
is_active = models.BooleanField(default=False)
- objects = CustomUserActiveManager()
- all_objects = CustomUserAllManager()
-
REQUIRED_FIELDS = ['email', "username", "password"]
@@ -125,14 +86,10 @@ class CustomUser(SoftDeleteModel, AbstractUser):
def delete(self, *args, **kwargs):
self.is_active = False
- #self.orders.all().update(is_deleted=True, deleted_at=timezone.now())
-
return super().delete(*args, **kwargs)
def save(self, *args, **kwargs):
- is_new = self.pk is None # check BEFORE saving
-
- if is_new:
+ if self.pk is None: # if newely created user
if self.is_superuser or self.role == "admin":
self.is_active = True
diff --git a/backend/account/permissions.py b/backend/account/permissions.py
index 6916300..1ee5b7c 100644
--- a/backend/account/permissions.py
+++ b/backend/account/permissions.py
@@ -1,41 +1,25 @@
+from urllib import request
from rest_framework.permissions import BasePermission, SAFE_METHODS
from rest_framework.permissions import IsAuthenticated
from rest_framework_api_key.permissions import HasAPIKey
-#Podle svého uvážení (NEPOUŽÍVAT!!!)
-class RolePermission(BasePermission):
- allowed_roles = []
-
- def has_permission(self, request, view):
- # Je uživatel přihlášený a má roli z povolených?
- user_has_role = (
- request.user and
- request.user.is_authenticated and
- getattr(request.user, "role", None) in self.allowed_roles
- )
-
- # Má API klíč?
- has_api_key = HasAPIKey().has_permission(request, view)
-
-
- return user_has_role or has_api_key
-
-
#TOHLE POUŽÍT!!!
#Prostě stačí vložit: RoleAllowed('seller','cityClerk')
def RoleAllowed(*roles):
"""
Allows safe methods for any authenticated user.
Allows unsafe methods only for users with specific roles.
+ Allows access if a valid API key is provided.
Args:
- RolerAllowed('admin', 'user')
+ RoleAllowed('admin', 'user')
"""
class SafeOrRolePermission(BasePermission):
-
-
def has_permission(self, request, view):
+ # Má API klíč?
+ has_api_key = HasAPIKey().has_permission(request, view)
+
# Allow safe methods for any authenticated user
if request.method in SAFE_METHODS:
return IsAuthenticated().has_permission(request, view)
diff --git a/backend/account/templates/emails/advertisment.html b/backend/account/templates/emails/advertisment.html
new file mode 100644
index 0000000..0ee4e75
--- /dev/null
+++ b/backend/account/templates/emails/advertisment.html
@@ -0,0 +1,137 @@
+
+
+
+
+
+ |
+ Nabídka tvorby webových stránek
+ |
+
+
+ |
+
+ Jsme malý tým, který se snaží prorazit a přinášet moderní řešení za férové
+ ceny.
+ Nabízíme také levný hosting a SSL zabezpečení zdarma.
+
+
+ Dbáme na bezpečnost, používáme moderní frameworky
+ a rozhodně nejsme součástí „gerontosaurů“ – PHP nepoužíváme.
+
+ |
+
+
+
+
+ |
+ Balíčky
+ |
+
+
+
+
+
+ BASIC
+
+ - Jednoduchá prezentační webová stránka
+ - Moderní a responzivní design (PC, tablety, mobily)
+ - Maximalní počet stránek: 5
+ - Použítí vlastní domény a SSL certifikát zdarma
+
+
+ Cena: 5 000 Kč
+ (jednorázově) + 100 Kč / měsíc
+ |
+
+
+
+
+
+ STANDARD
+
+ - Vše z balíčku BASIC
+ - Kontaktní formulář, který posílá pobídky na váš email
+ - Větší priorita při řešení problémů a rychlejší vývoj (cca 2 týdny)
+ - Základní SEO
+ - Maximální počet stránek: 10
+
+
+ Cena: 7 500 Kč (jednorázově) + 250 Kč / měsíc
+ |
+
+
+
+
+
+ PREMIUM
+
+ - Vše z balíčku STANDARD
+ - Registrace firmy do Google Business Profile
+ - Pokročilé SEO (klíčová slova, podpora pro slepce, čtečky)
+ - Měsíční report návštěvnosti webu
+ - Možnost drobných úprav (texty, fotky)
+ - Neomezený počet stránek
+
+
+ Cena: od 9 500 Kč (jednorázově) + 400 Kč / měsíc
+ |
+
+
+
+
+
+ CUSTOM
+
+ - Kompletně na míru podle potřeb
+ - Možnost e-shopu, rezervačního systému, managment
+ - Integrace jakéhokoliv API
+ - Integrace platební brány (např. Stripe, Platba QR kódem, atd.)
+ - Pokročilé SEO
+ - Marketing skrz Google Ads
+
+
+ Cena: dohodou
+ |
+
+
+ |
+
+
+
\ No newline at end of file
diff --git a/backend/advertisement/__init__.py b/backend/advertisement/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/advertisement/admin.py b/backend/advertisement/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/backend/advertisement/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/backend/advertisement/apps.py b/backend/advertisement/apps.py
new file mode 100644
index 0000000..700e777
--- /dev/null
+++ b/backend/advertisement/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class AdvertisementConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'advertisement'
diff --git a/backend/advertisement/migrations/__init__.py b/backend/advertisement/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/advertisement/models.py b/backend/advertisement/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/backend/advertisement/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/backend/advertisement/tasks.py b/backend/advertisement/tasks.py
new file mode 100644
index 0000000..a5bec03
--- /dev/null
+++ b/backend/advertisement/tasks.py
@@ -0,0 +1,2 @@
+#udělat zasílaní reklamních emailů uživatelům.
+#newletter --> když se vytvoří nový record s reklamou email se uloží pomocí zaškrtnutí tlačítka v záznamu
\ No newline at end of file
diff --git a/backend/advertisement/tests.py b/backend/advertisement/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/backend/advertisement/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/backend/advertisement/views.py b/backend/advertisement/views.py
new file mode 100644
index 0000000..91ea44a
--- /dev/null
+++ b/backend/advertisement/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/backend/commerce/__init__.py b/backend/commerce/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/commerce/admin.py b/backend/commerce/admin.py
new file mode 100644
index 0000000..d76afc4
--- /dev/null
+++ b/backend/commerce/admin.py
@@ -0,0 +1,17 @@
+from django.contrib import admin
+from .models import Carrier, Order
+# Register your models here.
+
+
+@admin.register(Carrier)
+class CarrierAdmin(admin.ModelAdmin):
+ list_display = ("name", "price", "api_id")
+ search_fields = ("name", "api_id")
+
+
+@admin.register(Order)
+class OrderAdmin(admin.ModelAdmin):
+ list_display = ("id", "product", "carrier", "quantity", "total_price", "status", "created_at")
+ list_filter = ("status", "created_at")
+ search_fields = ("stripe_session_id",)
+ readonly_fields = ("total_price", "status", "stripe_session_id", "created_at", "updated_at")
diff --git a/backend/commerce/apps.py b/backend/commerce/apps.py
new file mode 100644
index 0000000..b996052
--- /dev/null
+++ b/backend/commerce/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class CommerceConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'commerce'
diff --git a/backend/commerce/migrations/__init__.py b/backend/commerce/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/commerce/models.py b/backend/commerce/models.py
new file mode 100644
index 0000000..6a5eb9d
--- /dev/null
+++ b/backend/commerce/models.py
@@ -0,0 +1,40 @@
+from django.db import models
+
+class Product(models.Model):
+ name = models.CharField(max_length=200)
+ description = models.TextField(blank=True)
+ price = models.DecimalField(max_digits=10, decimal_places=2)
+ currency = models.CharField(max_length=10, default="czk")
+ stock = models.PositiveIntegerField(default=0)
+ is_active = models.BooleanField(default=True)
+ default_carrier = models.ForeignKey(
+ "Carrier", on_delete=models.SET_NULL, null=True, blank=True, related_name="default_for_products"
+ )
+ created_at = models.DateTimeField(auto_now_add=True)
+
+ @property
+ def available(self):
+ return self.is_active and self.stock > 0
+
+ def __str__(self):
+ return f"{self.name} ({self.price} {self.currency.upper()})"
+
+
+# Dopravci a způsoby dopravy
+from django.db import models
+
+class Carrier(models.Model):
+ name = models.CharField(max_length=100) # název dopravce (Zásilkovna, Česká pošta…)
+ base_price = models.DecimalField(max_digits=10, decimal_places=2, default=0) # základní cena dopravy
+ delivery_time = models.CharField(max_length=100, blank=True) # např. "2–3 pracovní dny"
+ is_active = models.BooleanField(default=True)
+
+ # pole pro logo
+ logo = models.ImageField(upload_to="carriers/", blank=True, null=True)
+
+ # pole pro propojení s externím API (např. ID služby u Zásilkovny)
+ external_id = models.CharField(max_length=50, blank=True, null=True)
+
+ def __str__(self):
+ return f"{self.name} ({self.base_price} Kč)"
+
diff --git a/backend/commerce/serializers.py b/backend/commerce/serializers.py
new file mode 100644
index 0000000..9ff1b14
--- /dev/null
+++ b/backend/commerce/serializers.py
@@ -0,0 +1,26 @@
+from rest_framework import serializers
+from .models import Carrier
+
+class CarrierSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Carrier
+ fields = [
+ "id", "name", "base_price", "delivery_time",
+ "is_active", "logo", "external_id"
+ ]
+
+
+from rest_framework import serializers
+from .models import Product, Carrier, Order
+
+
+class ProductSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Product
+ fields = "__all__"
+
+
+class CarrierSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Carrier
+ fields = "__all__"
\ No newline at end of file
diff --git a/backend/commerce/tests.py b/backend/commerce/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/backend/commerce/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/backend/commerce/views.py b/backend/commerce/views.py
new file mode 100644
index 0000000..91ea44a
--- /dev/null
+++ b/backend/commerce/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/backend/thirdparty/stripe/admin.py b/backend/thirdparty/stripe/admin.py
index 8c38f3f..d23cc8e 100644
--- a/backend/thirdparty/stripe/admin.py
+++ b/backend/thirdparty/stripe/admin.py
@@ -1,3 +1,23 @@
from django.contrib import admin
+from .models import Order
-# Register your models here.
+@admin.register(Order)
+class OrderAdmin(admin.ModelAdmin):
+ list_display = ("id", "amount", "currency", "status", "created_at")
+ list_filter = ("status", "currency", "created_at")
+ search_fields = ("id", "stripe_session_id", "stripe_payment_intent")
+ readonly_fields = ("created_at", "stripe_session_id", "stripe_payment_intent")
+
+ fieldsets = (
+ (None, {
+ "fields": ("amount", "currency", "status")
+ }),
+ ("Stripe info", {
+ "fields": ("stripe_session_id", "stripe_payment_intent"),
+ "classes": ("collapse",),
+ }),
+ ("Metadata", {
+ "fields": ("created_at",),
+ }),
+ )
+ ordering = ("-created_at",)
diff --git a/backend/thirdparty/stripe/models.py b/backend/thirdparty/stripe/models.py
index 71a8362..61a83c1 100644
--- a/backend/thirdparty/stripe/models.py
+++ b/backend/thirdparty/stripe/models.py
@@ -1,3 +1,21 @@
from django.db import models
# Create your models here.
+
+class Order(models.Model):
+ STATUS_CHOICES = [
+ ("pending", "Pending"),
+ ("paid", "Paid"),
+ ("failed", "Failed"),
+ ("cancelled", "Cancelled"),
+ ]
+
+ amount = models.DecimalField(max_digits=10, decimal_places=2)
+ currency = models.CharField(max_length=10, default="czk")
+ status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="pending")
+ stripe_session_id = models.CharField(max_length=255, blank=True, null=True)
+ stripe_payment_intent = models.CharField(max_length=255, blank=True, null=True)
+ created_at = models.DateTimeField(auto_now_add=True)
+
+ def __str__(self):
+ return f"Order {self.id} - {self.status}"
\ No newline at end of file
diff --git a/backend/thirdparty/stripe/serializers.py b/backend/thirdparty/stripe/serializers.py
index 7ad1635..f88ce06 100644
--- a/backend/thirdparty/stripe/serializers.py
+++ b/backend/thirdparty/stripe/serializers.py
@@ -1,12 +1,54 @@
from rest_framework import serializers
+from rest_framework import serializers
+from .models import Product, Carrier, Order
-class StripeCheckoutRequestSerializer(serializers.Serializer):
- amount = serializers.DecimalField(max_digits=12, decimal_places=2, min_value=0.01)
- product_name = serializers.CharField(required=False, default="Example Product")
- success_url = serializers.URLField(required=False)
- cancel_url = serializers.URLField(required=False)
+from ...commerce.serializers import ProductSerializer, CarrierSerializer
-class StripeCheckoutResponseSerializer(serializers.Serializer):
- url = serializers.URLField()
+class OrderSerializer(serializers.ModelSerializer):
+ product = ProductSerializer(read_only=True)
+ product_id = serializers.PrimaryKeyRelatedField(
+ queryset=Product.objects.all(), source="product", write_only=True
+ )
+ carrier = CarrierSerializer(read_only=True)
+ carrier_id = serializers.PrimaryKeyRelatedField(
+ queryset=Carrier.objects.all(), source="carrier", write_only=True
+ )
+
+ class Meta:
+ model = Order
+ fields = [
+ "id",
+ "product", "product_id",
+ "carrier", "carrier_id",
+ "quantity",
+ "total_price",
+ "status",
+ "stripe_session_id",
+ "created_at",
+ "updated_at",
+ ]
+ read_only_fields = ("total_price", "status", "stripe_session_id", "created_at", "updated_at")
+
+ queryset=Product.objects.all(), source="product", write_only=True
+
+ carrier = CarrierSerializer(read_only=True)
+ carrier_id = serializers.PrimaryKeyRelatedField(
+ queryset=Carrier.objects.all(), source="carrier", write_only=True
+ )
+
+ class Meta:
+ model = Order
+ fields = [
+ "id",
+ "product", "product_id",
+ "carrier", "carrier_id",
+ "quantity",
+ "total_price",
+ "status",
+ "stripe_session_id",
+ "created_at",
+ "updated_at",
+ ]
+ read_only_fields = ("total_price", "status", "stripe_session_id", "created_at", "updated_at")
diff --git a/backend/thirdparty/stripe/urls.py b/backend/thirdparty/stripe/urls.py
index 02287d5..58182eb 100644
--- a/backend/thirdparty/stripe/urls.py
+++ b/backend/thirdparty/stripe/urls.py
@@ -1,6 +1,6 @@
from django.urls import path
-from .views import StripeCheckoutCZKView
+from .views import CreateCheckoutSessionView
urlpatterns = [
- path('checkout/', StripeCheckoutCZKView.as_view(), name='stripe-checkout-czk'),
+ path("orders/create-checkout/", CreateCheckoutSessionView.as_view(), name="create-checkout"),
]
\ No newline at end of file
diff --git a/backend/thirdparty/stripe/views.py b/backend/thirdparty/stripe/views.py
index 8028b08..278bf0c 100644
--- a/backend/thirdparty/stripe/views.py
+++ b/backend/thirdparty/stripe/views.py
@@ -1,71 +1,73 @@
-import stripe
-import os
-from rest_framework.views import APIView
-from rest_framework.permissions import IsAuthenticated
+from django.views.decorators.csrf import csrf_exempt
+from django.conf import settings
+from django.http import HttpResponse
+
+from rest_framework import generics
from rest_framework.response import Response
+from rest_framework.views import APIView
-from drf_spectacular.utils import extend_schema, OpenApiResponse, OpenApiExample, OpenApiParameter
-from .serializers import (
- StripeCheckoutRequestSerializer,
- StripeCheckoutResponseSerializer,
-)
+from .models import Order
+from .serializers import OrderSerializer
-stripe.api_key = os.getenv("STRIPE_SECRET_KEY")
+import stripe
+stripe.api_key = settings.STRIPE_SECRET_KEY # uložený v .env
-
-class StripeCheckoutCZKView(APIView):
- permission_classes = [IsAuthenticated]
-
- @extend_schema(
- tags=["Stripe"],
- summary="Create Stripe Checkout session in CZK",
- description="Creates a Stripe Checkout session for payment in Czech Koruna (CZK). Requires authentication.",
- request=StripeCheckoutRequestSerializer,
- responses={
- 200: OpenApiResponse(response=StripeCheckoutResponseSerializer, description="Stripe Checkout session URL returned successfully."),
- 400: OpenApiResponse(description="Amount is required or invalid."),
- },
- examples=[
- OpenApiExample(
- "Success",
- value={"url": "https://checkout.stripe.com/pay/cs_test_123456"},
- response_only=True,
- status_codes=["200"],
- ),
- OpenApiExample(
- "Missing amount",
- value={"error": "Amount is required"},
- response_only=True,
- status_codes=["400"],
- ),
- ]
- )
+class CreateCheckoutSessionView(APIView):
def post(self, request):
- serializer = StripeCheckoutRequestSerializer(data=request.data)
- if not serializer.is_valid():
- return Response(serializer.errors, status=400)
+ serializer = OrderSerializer(data=request.data) #obecný serializer
+ serializer.is_valid(raise_exception=True)
- amount = serializer.validated_data.get("amount")
- product_name = serializer.validated_data.get("product_name", "Example Product")
- success_url = serializer.validated_data.get("success_url", "https://yourfrontend.com/success")
- cancel_url = serializer.validated_data.get("cancel_url", "https://yourfrontend.com/cancel")
- # Stripe expects amount in the smallest currency unit (haléř = 1/100 CZK)
- amount_in_haler = int(amount * 100)
- session = stripe.checkout.Session.create(
- payment_method_types=['card'],
- line_items=[{
- 'price_data': {
- 'currency': 'czk',
- 'product_data': {
- 'name': product_name,
- },
- 'unit_amount': amount_in_haler,
- },
- 'quantity': 1,
- }],
- mode='payment',
- success_url=success_url,
- cancel_url=cancel_url,
- customer_email=getattr(request.user, 'email', None)
+ order = Order.objects.create(
+ amount=serializer.validated_data["amount"],
+ currency=serializer.validated_data.get("currency", "czk"),
)
- return Response({"url": session.url})
+
+ # Vytvoření Stripe Checkout Session
+ session = stripe.checkout.Session.create(
+ payment_method_types=["card"],
+ line_items=[{
+ "price_data": {
+ "currency": order.currency,
+ "product_data": {"name": f"Order {order.id}"},
+ "unit_amount": int(order.amount * 100), # v centech
+ },
+ "quantity": 1,
+ }],
+ mode="payment",
+ success_url=request.build_absolute_uri(f"/payment/success/{order.id}"),
+ cancel_url=request.build_absolute_uri(f"/payment/cancel/{order.id}"),
+ )
+
+ order.stripe_session_id = session.id
+ order.stripe_payment_intent = session.payment_intent
+ order.save()
+
+ data = OrderSerializer(order).data
+ data["checkout_url"] = session.url
+ return Response(data)
+
+
+
+
+
+@csrf_exempt
+def stripe_webhook(request):
+ payload = request.body
+ sig_header = request.META.get("HTTP_STRIPE_SIGNATURE")
+ event = None
+
+ try:
+ event = stripe.Webhook.construct_event(
+ payload, sig_header, settings.STRIPE_WEBHOOK_SECRET
+ )
+ except stripe.error.SignatureVerificationError:
+ return HttpResponse(status=400)
+
+ if event["type"] == "checkout.session.completed":
+ session = event["data"]["object"]
+ order = Order.objects.filter(stripe_session_id=session.get("id")).first()
+ if order:
+ order.status = "paid"
+ order.save()
+
+ return HttpResponse(status=200)
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 08a3ac9..7e2f1cc 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -11,6 +11,12 @@
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
+ --c-background: #031D44; /*background*/
+ --c-background-light: #04395E; /*background-highlight*/
+ --c-boxes: #24719f;; /*boxes*/
+ --c-lines: #87a9da; /*lines*/
+ --c-text: #CAF0F8; /*text*/
+ --c-other: #70A288; /*other*/
}
a {