Refactor Stripe payment handling and order status

Refactored Stripe payment integration to use a dedicated Stripe model for session data, updating the PaymentSerializer accordingly. Renamed Order.Status to Order.OrderStatus for clarity. Updated configuration app to ensure SiteConfiguration singleton is created post-migration. Removed obsolete seed_app_config command from docker-compose. Adjusted frontend API generated files and moved orval config.
This commit is contained in:
2025-12-19 14:08:40 +01:00
parent 2498386477
commit 713c94d7e9
12 changed files with 377 additions and 56 deletions

View File

@@ -104,7 +104,7 @@ class ProductImage(models.Model):
# ------------------ OBJEDNÁVKY ------------------ # ------------------ OBJEDNÁVKY ------------------
class Order(models.Model): class Order(models.Model):
class Status(models.TextChoices): class OrderStatus(models.TextChoices):
CREATED = "created", "cz#Vytvořeno" CREATED = "created", "cz#Vytvořeno"
CANCELLED = "cancelled", "cz#Zrušeno" CANCELLED = "cancelled", "cz#Zrušeno"
COMPLETED = "completed", "cz#Dokončeno" COMPLETED = "completed", "cz#Dokončeno"
@@ -113,7 +113,7 @@ class Order(models.Model):
REFUNDED = "refunded", "cz#Vráceno" REFUNDED = "refunded", "cz#Vráceno"
status = models.CharField( status = models.CharField(
max_length=20, choices=Status.choices, null=True, blank=True, default=Status.CREATED max_length=20, choices=OrderStatus.choices, null=True, blank=True, default=OrderStatus.CREATED
) )
# Stored order grand total; recalculated on save # Stored order grand total; recalculated on save

View File

@@ -143,6 +143,9 @@ class OrderItemCreateSerializer(serializers.Serializer):
# -- PAYMENT -- # -- PAYMENT --
class PaymentSerializer(serializers.ModelSerializer): class PaymentSerializer(serializers.ModelSerializer):
stripe_session_id = serializers.CharField(source='stripe.stripe_session_id', read_only=True)
stripe_payment_intent = serializers.CharField(source='stripe.stripe_payment_intent', read_only=True)
stripe_session_url = serializers.URLField(source='stripe.stripe_session_url', read_only=True)
class Meta: class Meta:
model = Payment model = Payment
@@ -184,15 +187,15 @@ class PaymentSerializer(serializers.ModelSerializer):
if payment.payment_method == Payment.PAYMENT.STRIPE: if payment.payment_method == Payment.PAYMENT.STRIPE:
session = StripeClient.create_checkout_session(order) session = StripeClient.create_checkout_session(order)
payment.stripe_session_id = session.id stripe_instance = StripeModel.objects.create(
payment.stripe_payment_intent = session.payment_intent stripe_session_id=session.id,
payment.stripe_session_url = session.url stripe_payment_intent=session.payment_intent,
stripe_session_url=session.url,
status=StripeModel.STATUS_CHOICES.PENDING
)
payment.save(update_fields=[ payment.stripe = stripe_instance
"stripe_session_id", payment.save(update_fields=["stripe"])
"stripe_payment_intent",
"stripe_session_url",
])
return payment return payment

View File

@@ -1,21 +1,22 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.db.utils import OperationalError, ProgrammingError from django.db.models.signals import post_migrate
def create_site_config(sender, **kwargs):
"""
Ensure the SiteConfiguration singleton exists after migrations.
"""
from .models import SiteConfiguration
try:
SiteConfiguration.get_solo()
except Exception:
pass
class ConfigurationConfig(AppConfig): class ConfigurationConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'configuration' name = 'configuration'
def ready(self): def ready(self):
"""Ensure the SiteConfiguration singleton exists at startup. # Spustí create_site_config po dokončení migrací
Wrapped in broad DB error handling so that commands like post_migrate.connect(create_site_config, sender=self)
makemigrations/migrate don't fail when the table does not yet exist.
"""
try:
from .models import SiteConfiguration # local import to avoid premature app registry access
SiteConfiguration.get_solo() # creates if missing
except (OperationalError, ProgrammingError):
# DB not ready (e.g., before initial migrate); ignore silently
pass

View File

@@ -0,0 +1 @@
filler

View File

@@ -19,7 +19,6 @@ services:
python manage.py migrate --verbosity 3 --noinput && python manage.py migrate --verbosity 3 --noinput &&
python manage.py check && python manage.py check &&
python manage.py collectstatic --clear --noinput --verbosity 3 && python manage.py collectstatic --clear --noinput --verbosity 3 &&
python manage.py seed_app_config &&
gunicorn -k uvicorn.workers.UvicornWorker vontor_cz.asgi:application --bind 0.0.0.0:8000" gunicorn -k uvicorn.workers.UvicornWorker vontor_cz.asgi:application --bind 0.0.0.0:8000"
ports: ports:
- "8000:8000" - "8000:8000"

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@
"build": "tsc -b && tsc -b && vite build", "build": "tsc -b && tsc -b && vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview", "preview": "vite preview",
"api:gen": "orval --config orval.config.ts" "api:gen": "orval --config src/orval.config.ts"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.16", "@tailwindcss/vite": "^4.1.16",
@@ -16,7 +16,6 @@
"@types/react-router": "^5.1.20", "@types/react-router": "^5.1.20",
"axios": "^1.13.0", "axios": "^1.13.0",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"orval": "^7.13.2",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
@@ -26,8 +25,7 @@
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.33.0", "@eslint/js": "^9.33.0",
"@tailwindcss/postcss": "^4.1.17", "@tailwindcss/postcss": "^4.1.17",
"@types/axios": "^0.9.36", "@types/node": "^24.10.4",
"@types/node": "^24.10.1",
"@types/react": "^19.1.10", "@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7", "@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^5.0.0", "@vitejs/plugin-react": "^5.0.0",
@@ -36,6 +34,7 @@
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0", "globals": "^16.3.0",
"orval": "^7.13.2",
"typescript": "~5.8.3", "typescript": "~5.8.3",
"typescript-eslint": "^8.39.1", "typescript-eslint": "^8.39.1",
"vite": "^7.1.2" "vite": "^7.1.2"

View File

@@ -1 +0,0 @@
v tehle složce se vygeneruje schema

View File

@@ -0,0 +1 @@
filler

View File

@@ -1,4 +1,4 @@
import axios from "axios"; import axios, { type AxiosRequestConfig } from "axios";
// použij tohle pro API vyžadující autentizaci // použij tohle pro API vyžadující autentizaci
export const privateApi = axios.create({ export const privateApi = axios.create({
@@ -25,3 +25,11 @@ privateApi.interceptors.response.use(
return Promise.reject(error); return Promise.reject(error);
} }
); );
export const privateMutator = async <T>(
config: AxiosRequestConfig
): Promise<T> => {
const response = await privateApi.request<T>(config);
return response.data;
};

View File

@@ -1,7 +1,16 @@
import axios from "axios"; import axios, { type AxiosRequestConfig } from "axios";
// použij tohle pro veřejné API nevyžadující autentizaci // použij tohle pro veřejné API nevyžadující autentizaci
export const publicApi = axios.create({ export const publicApi = axios.create({
baseURL: "/api/", baseURL: "/api/",
withCredentials: false, // veřejné API NEPOSÍLÁ cookies withCredentials: false, // veřejné API NEPOSÍLÁ cookies
}); });
// ⬇⬇⬇ TOHLE JE TEN MUTATOR ⬇⬇⬇
export const publicMutator = async <T>(
config: AxiosRequestConfig
): Promise<T> => {
const response = await publicApi.request<T>(config);
return response.data;
};

View File

@@ -1,8 +1,7 @@
import { defineConfig } from "orval"; import { defineConfig } from "orval";
import "dotenv/config"; import "dotenv/config";
import {process} from "node:process";
const backendUrl = process.env.VITE_API_BASE_URL || "http://localhost:8000"; const backendUrl = process.env.VITE_BACKEND_URL || "http://localhost:8000";
// může se hodit pokud nechceme při buildu generovat klienta (nechat false pro produkci nebo vynechat) // může se hodit pokud nechceme při buildu generovat klienta (nechat false pro produkci nebo vynechat)
const SKIP_ORVAL = process.env.SKIP_ORVAL === "true"; const SKIP_ORVAL = process.env.SKIP_ORVAL === "true";
@@ -23,8 +22,8 @@ export default defineConfig({
}, },
}, },
output: { output: {
target: "src/api/generated/public.ts", target: "api/generated/public.ts",
schemas: "src/api/generated/public/models", schemas: "api/generated/public/models",
mode: "tags", mode: "tags",
clean: true, clean: true,
@@ -34,8 +33,8 @@ export default defineConfig({
override: { override: {
mutator: { mutator: {
path: "src/api/publicClient.ts", //IMPORTANTE path: "api/publicClient.ts", //IMPORTANTE
name: "publicApi", name: "publicMutator",
}, },
}, },
}, },
@@ -45,13 +44,11 @@ export default defineConfig({
}, },
private: { private: {
input: { input: {
target: `${backendUrl}/api/schema/` target: `${backendUrl}/api/schema/`,
// No filters, include all endpoints
}, },
output: { output: {
target: "src/api/generated/private.ts", //IMPORTANTE target: "api/generated/private.ts", //IMPORTANTE
schemas: "src/api/generated/private/models", schemas: "api/generated/private/models",
mode: "tags", mode: "tags",
clean: true, clean: true,
@@ -61,8 +58,8 @@ export default defineConfig({
override: { override: {
mutator: { mutator: {
path: "src/api/privateClient.ts", path: "api/privateClient.ts",
name: "privateApi", name: "privateMutator",
}, },
}, },
}, },