integrace api, stripe, vytvoření commecre app
This commit is contained in:
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal 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}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
137
backend/account/templates/emails/advertisment.html
Normal file
137
backend/account/templates/emails/advertisment.html
Normal 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>
|
||||||
0
backend/advertisement/__init__.py
Normal file
0
backend/advertisement/__init__.py
Normal file
3
backend/advertisement/admin.py
Normal file
3
backend/advertisement/admin.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
backend/advertisement/apps.py
Normal file
6
backend/advertisement/apps.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AdvertisementConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'advertisement'
|
||||||
0
backend/advertisement/migrations/__init__.py
Normal file
0
backend/advertisement/migrations/__init__.py
Normal file
3
backend/advertisement/models.py
Normal file
3
backend/advertisement/models.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
2
backend/advertisement/tasks.py
Normal file
2
backend/advertisement/tasks.py
Normal 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
|
||||||
3
backend/advertisement/tests.py
Normal file
3
backend/advertisement/tests.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
3
backend/advertisement/views.py
Normal file
3
backend/advertisement/views.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
0
backend/commerce/__init__.py
Normal file
0
backend/commerce/__init__.py
Normal file
17
backend/commerce/admin.py
Normal file
17
backend/commerce/admin.py
Normal 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
6
backend/commerce/apps.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CommerceConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'commerce'
|
||||||
0
backend/commerce/migrations/__init__.py
Normal file
0
backend/commerce/migrations/__init__.py
Normal file
40
backend/commerce/models.py
Normal file
40
backend/commerce/models.py
Normal 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ř. "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č)"
|
||||||
|
|
||||||
26
backend/commerce/serializers.py
Normal file
26
backend/commerce/serializers.py
Normal 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__"
|
||||||
3
backend/commerce/tests.py
Normal file
3
backend/commerce/tests.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
3
backend/commerce/views.py
Normal file
3
backend/commerce/views.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
22
backend/thirdparty/stripe/admin.py
vendored
22
backend/thirdparty/stripe/admin.py
vendored
@@ -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",)
|
||||||
|
|||||||
18
backend/thirdparty/stripe/models.py
vendored
18
backend/thirdparty/stripe/models.py
vendored
@@ -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}"
|
||||||
56
backend/thirdparty/stripe/serializers.py
vendored
56
backend/thirdparty/stripe/serializers.py
vendored
@@ -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")
|
||||||
|
|||||||
4
backend/thirdparty/stripe/urls.py
vendored
4
backend/thirdparty/stripe/urls.py
vendored
@@ -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"),
|
||||||
]
|
]
|
||||||
130
backend/thirdparty/stripe/views.py
vendored
130
backend/thirdparty/stripe/views.py
vendored
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user