integrace api, stripe, vytvoření commecre app

This commit is contained in:
2025-10-05 23:41:14 +02:00
parent f5cf8bbaa7
commit 10796dcb31
26 changed files with 436 additions and 140 deletions

15
.vscode/launch.json vendored Normal file
View File

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

View File

@@ -16,15 +16,6 @@ import logging
logger = logging.getLogger(__name__) 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): class CustomUser(SoftDeleteModel, AbstractUser):
@@ -67,33 +58,6 @@ class CustomUser(SoftDeleteModel, AbstractUser):
email = models.EmailField(unique=True, db_index=True) email = models.EmailField(unique=True, db_index=True)
create_time = models.DateTimeField(auto_now_add=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) city = models.CharField(null=True, blank=True, max_length=100)
street = models.CharField(null=True, blank=True, max_length=200) street = models.CharField(null=True, blank=True, max_length=200)
@@ -113,9 +77,6 @@ class CustomUser(SoftDeleteModel, AbstractUser):
is_active = models.BooleanField(default=False) is_active = models.BooleanField(default=False)
objects = CustomUserActiveManager()
all_objects = CustomUserAllManager()
REQUIRED_FIELDS = ['email', "username", "password"] REQUIRED_FIELDS = ['email', "username", "password"]
@@ -125,14 +86,10 @@ class CustomUser(SoftDeleteModel, AbstractUser):
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
self.is_active = False self.is_active = False
#self.orders.all().update(is_deleted=True, deleted_at=timezone.now())
return super().delete(*args, **kwargs) return super().delete(*args, **kwargs)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
is_new = self.pk is None # check BEFORE saving if self.pk is None: # if newely created user
if is_new:
if self.is_superuser or self.role == "admin": if self.is_superuser or self.role == "admin":
self.is_active = True self.is_active = True

View File

@@ -1,41 +1,25 @@
from urllib import request
from rest_framework.permissions import BasePermission, SAFE_METHODS from rest_framework.permissions import BasePermission, SAFE_METHODS
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework_api_key.permissions import HasAPIKey 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!!! #TOHLE POUŽÍT!!!
#Prostě stačí vložit: RoleAllowed('seller','cityClerk') #Prostě stačí vložit: RoleAllowed('seller','cityClerk')
def RoleAllowed(*roles): def RoleAllowed(*roles):
""" """
Allows safe methods for any authenticated user. Allows safe methods for any authenticated user.
Allows unsafe methods only for users with specific roles. Allows unsafe methods only for users with specific roles.
Allows access if a valid API key is provided.
Args: Args:
RolerAllowed('admin', 'user') RoleAllowed('admin', 'user')
""" """
class SafeOrRolePermission(BasePermission): class SafeOrRolePermission(BasePermission):
def has_permission(self, request, view): def has_permission(self, request, view):
# Má API klíč?
has_api_key = HasAPIKey().has_permission(request, view)
# Allow safe methods for any authenticated user # Allow safe methods for any authenticated user
if request.method in SAFE_METHODS: if request.method in SAFE_METHODS:
return IsAuthenticated().has_permission(request, view) return IsAuthenticated().has_permission(request, view)

View File

@@ -0,0 +1,137 @@
<table style="background-color:#031D44; font-family:'Exo', Arial, sans-serif; width:100%;" align="center" border="0"
cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="padding:20px;">
<table border="0" cellspacing="45" cellpadding="0" style="max-width:100%;" align="center">
<tr>
<td align="center"
style="padding:20px; color:#ffffff; font-size:30px; font-weight:bold; border-radius:8px; text-decoration:underline">
Nabídka tvorby webových stránek
</td>
</tr>
<tr>
<td style="color:#CAF0F8; border-radius:8px; font-size:25px; line-height:1.6;">
<p style="margin:0;">
Jsme <strong>malý tým</strong>, který se snaží prorazit a přinášet moderní řešení za férové
ceny.
Nabízíme také <strong>levný hosting</strong> a <strong>SSL zabezpečení zdarma</strong>.
</p>
<p style="margin:10px 0 0;">
Dbáme na <strong>bezpečnost</strong>, používáme <strong>moderní frameworky</strong>
a rozhodně nejsme součástí „gerontosaurů“ <strong>PHP nepoužíváme</strong>.
</p>
</td>
</tr>
<!-- Nadpis -->
<tr>
<td align="center"
style="padding-top:50px; color:#ffffff; font-size:35px; font-weight:bold; border-radius:8px; text-decoration:underline">
Balíčky
</td>
</tr>
<!-- BASIC -->
<tr>
<td
style="padding:35px; background:#3a8bb7; color:#CAF0F8; border-radius:20px; margin:20px 0; line-height:1.6;font-size: 18px; width: 450px;">
<h2 style="margin:0; color:#CAF0F8;">BASIC</h2>
<ul style="padding-left:20px; margin:10px 0;">
<li>Jednoduchá prezentační webová stránka</li>
<li>Moderní a responzivní design (PC, tablety, mobily)</li>
<li>Maximalní počet stránek: 5</li>
<li>Použítí vlastní domény a <span style="text-decoration: underline;">SSL certifikát zdarma</span></li>
</ul>
<p
style="font-size: 18px; border-radius:8px; width: fit-content;background-color:#24719f; padding:16px; color:#ffffff; font-weight:bold; margin:0;">
Cena: 5 000 Kč
(jednorázově) + 100 Kč / měsíc</p>
</td>
</tr>
<!-- STANDARD -->
<tr>
<td
style="padding:35px; background:#70A288; color:#ffffff; border-radius:20px; line-height:1.6; font-size:18px; width:450px;">
<h2 style="margin:0; color:#ffffff;">STANDARD</h2>
<ul style="padding-left:20px; margin:10px 0;">
<li>Vše z balíčku BASIC</li>
<li>Kontaktní formulář, který posílá pobídky na váš email</li>
<li>Větší priorita při řešení problémů a rychlejší vývoj (cca 2 týdny)</li>
<li>Základní SEO</li>
<li>Maximální počet stránek: 10</li>
</ul>
<p
style="font-size:18px; border-radius:8px; width: fit-content; background-color:#508845; padding:16px; color:#ffffff; font-weight:bold; margin:0;">
Cena: 7 500 Kč (jednorázově) + 250 Kč / měsíc</p>
</td>
</tr>
<!-- PREMIUM -->
<tr>
<td
style="padding:35px; background:#87a9da; color:#031D44; border-radius:20px; line-height:1.6; font-size:18px; width:450px;">
<h2 style="margin:0; color:#031D44;">PREMIUM</h2>
<ul style="padding-left:20px; margin:10px 0;">
<li>Vše z balíčku STANDARD</li>
<li>Registrace firmy do Google Business Profile</li>
<li>Pokročilé SEO (klíčová slova, podpora pro slepce, čtečky)</li>
<li>Měsíční report návštěvnosti webu</li>
<li>Možnost drobných úprav (texty, fotky)</li>
<li>Neomezený počet stránek</li>
</ul>
<p
style="font-size:18px; border-radius:8px; width: fit-content; background-color:#4c7bbd; padding:16px; color:#ffffff; font-weight:bold; margin:0;">
Cena: od 9 500 Kč (jednorázově) + 400 Kč / měsíc</p>
</td>
</tr>
<!-- CUSTOM -->
<tr>
<td
style="padding:35px; background:#04395E; color:#CAF0F8; border-radius:20px; line-height:1.6; font-size:18px; width:450px;">
<h2 style="margin:0; color:#CAF0F8;">CUSTOM</h2>
<ul style="padding-left:20px; margin:10px 0;">
<li>Kompletně na míru podle potřeb</li>
<li>Možnost e-shopu, rezervačního systému, managment</li>
<li>Integrace jakéhokoliv API</li>
<li>Integrace platební brány (např. Stripe, Platba QR kódem, atd.)</li>
<li>Pokročilé SEO</li>
<li>Marketing skrz Google Ads</li>
</ul>
<p
style="font-size:18px; border-radius:8px; width: fit-content; background-color:#216085; padding:16px; color:#ffffff; font-weight:bold; margin:0;">
Cena: dohodou</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
<table style="width: 100%;background-color:#031D44; font-family:'Exo', Arial, sans-serif;" align="center" border="0"
cellspacing="50" cellpadding="0" style="color: #031D44;">
<!-- Footer -->
<tr>
<td align="center"
style=" border-radius: 50px; background-color: hwb(201 23% 28% / 0); padding:30px; color:#CAF0F8;">
<p style="margin:0; font-size:25px; font-weight:bold;">Máte zájem o některý z balíčků?</p>
</td>
</tr>
<tr>
<td align="center" style=" border-radius: 50px; padding:30px; color:#CAF0F8;">
<p>Stačí odpovědět na tento e-mail nebo mě kontaktovat telefonicky:</p>
<p>
<a style="color:#CAF0F8; text-decoration:underline;" href="mailto:brunovontor@gmail.com">
brunovontor@gmail.com
</a>
</p>
<p>
<a style="color:#CAF0F8; text-decoration:underline;" href="tel:+420605512624">+420 605 512 624</a>
</p>
<p>
<a style="color:#CAF0F8; text-decoration:underline;" href="https://vontor.cz">vontor.cz</a>
</p>
</td>
</tr>
</table>

View File

View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class AdvertisementConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'advertisement'

View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

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

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

17
backend/commerce/admin.py Normal file
View File

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

6
backend/commerce/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class CommerceConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'commerce'

View File

View File

@@ -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ř. "23 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č)"

View File

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

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -1,3 +1,23 @@
from django.contrib import admin 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",)

View File

@@ -1,3 +1,21 @@
from django.db import models from django.db import models
# Create your models here. # 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}"

View File

@@ -1,12 +1,54 @@
from rest_framework import serializers from rest_framework import serializers
from rest_framework import serializers
from .models import Product, Carrier, Order
class StripeCheckoutRequestSerializer(serializers.Serializer): from ...commerce.serializers import ProductSerializer, CarrierSerializer
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)
class StripeCheckoutResponseSerializer(serializers.Serializer): class OrderSerializer(serializers.ModelSerializer):
url = serializers.URLField() 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")

View File

@@ -1,6 +1,6 @@
from django.urls import path from django.urls import path
from .views import StripeCheckoutCZKView from .views import CreateCheckoutSessionView
urlpatterns = [ urlpatterns = [
path('checkout/', StripeCheckoutCZKView.as_view(), name='stripe-checkout-czk'), path("orders/create-checkout/", CreateCheckoutSessionView.as_view(), name="create-checkout"),
] ]

View File

@@ -1,71 +1,73 @@
import stripe from django.views.decorators.csrf import csrf_exempt
import os from django.conf import settings
from rest_framework.views import APIView from django.http import HttpResponse
from rest_framework.permissions import IsAuthenticated
from rest_framework import generics
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView
from drf_spectacular.utils import extend_schema, OpenApiResponse, OpenApiExample, OpenApiParameter from .models import Order
from .serializers import ( from .serializers import OrderSerializer
StripeCheckoutRequestSerializer,
StripeCheckoutResponseSerializer,
)
stripe.api_key = os.getenv("STRIPE_SECRET_KEY") import stripe
stripe.api_key = settings.STRIPE_SECRET_KEY # uložený v .env
class CreateCheckoutSessionView(APIView):
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"],
),
]
)
def post(self, request): def post(self, request):
serializer = StripeCheckoutRequestSerializer(data=request.data) serializer = OrderSerializer(data=request.data) #obecný serializer
if not serializer.is_valid(): serializer.is_valid(raise_exception=True)
return Response(serializer.errors, status=400)
amount = serializer.validated_data.get("amount") order = Order.objects.create(
product_name = serializer.validated_data.get("product_name", "Example Product") amount=serializer.validated_data["amount"],
success_url = serializer.validated_data.get("success_url", "https://yourfrontend.com/success") currency=serializer.validated_data.get("currency", "czk"),
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)
) )
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)

View File

@@ -11,6 +11,12 @@
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -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 { a {