fix: Befunde aus Sicherheits- und Korrektheitsreview der Bienenpatenschaft
Docker Image bauen und veroeffentlichen / typecheck (push) Successful in 2m57s
Docker Image bauen und veroeffentlichen / lint (push) Successful in 3m7s
Docker Image bauen und veroeffentlichen / build-and-push (push) Canceled after 0s
Docker Image bauen und veroeffentlichen / test (push) Canceled after 1m22s
Docker Image bauen und veroeffentlichen / typecheck (push) Successful in 2m57s
Docker Image bauen und veroeffentlichen / lint (push) Successful in 3m7s
Docker Image bauen und veroeffentlichen / build-and-push (push) Canceled after 0s
Docker Image bauen und veroeffentlichen / test (push) Canceled after 1m22s
- Oeffentliche Kuendigung: exakte Identifikation, Limits je Vertrag und Adresse, Bestaetigung nur an die Kontoadresse mit Ruecknahme-Link (HMAC) - Vorschau: Body-Grenze beim Lesen, Parallelitaets- und Minutenlimit, Cache - Rabatte gelten nicht fuer Patenschaftspositionen - Abo aktiv erst nach bestaetigter Zahlung, offene Abschluesse zaehlen zum Kontingent und werden nach 24 h aufgeraeumt, Sitzung 58 Minuten - Konto-Loeschung nur ohne laufendes Abo, Urkunden-Dateien werden entfernt - Abo-Lieferungen mit Zahlungszeile (DHL-Marke moeglich) - Shop zeigt freien Bestand nach Abo-Reservierung - Storno/volle Erstattung widerruft die Patenschaft (Status WIDERRUFEN) - Umsatzsteuer der Patenschaft per VAT_RATE_PERCENT_PATENSCHAFT (19 %) - Digitale Bestellungen archivieren sich, Kalendertage in Europe/Berlin, Monatsenden begrenzt, Datenauskunft um Patenschaften ergaenzt Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
f227aeb8f9
commit
515fca1c16
97 files changed
+2979
-254
No files matched your search
@@ -11,7 +11,7 @@ import { isTrustedOrigin } from "@/lib/csrf";
|
||||
import { verifyTurnstileToken } from "@/lib/turnstile";
|
||||
import { getShippingCostCents, getShippingRegion } from "@/lib/shipping";
|
||||
import { findCustomsExcludedArea, isCountryAllowed } from "@/lib/shipping-region";
|
||||
import { isDiscountCodeValid, calculateDiscountCents } from "@/lib/discount-calc";
|
||||
import { isDiscountCodeValid, calculateDiscountCents, discountBase } from "@/lib/discount-calc";
|
||||
import { ensureBulkDiscountCode } from "@/lib/bulk-discount";
|
||||
import { releaseUnpaidOrder } from "@/lib/order-release";
|
||||
import {
|
||||
@@ -24,7 +24,7 @@ import { vatRateForProduct } from "@/lib/vat";
|
||||
import { getLocale } from "@/i18n/locale";
|
||||
import { expandCheckoutItems, type CheckoutProduct } from "@/lib/sponsorship/checkout-expand";
|
||||
import { getOffersByProductIds } from "@/lib/sponsorship/offers";
|
||||
import { reservedJarsForProduct } from "@/lib/sponsorship-subscription/reservation";
|
||||
import { reservedJarsByProduct } from "@/lib/sponsorship-subscription/reservation";
|
||||
|
||||
class OutOfStockError extends Error {}
|
||||
// Der fruehe pendingUses-Check weiter unten liest ausserhalb jeder Transaktion
|
||||
@@ -332,13 +332,13 @@ export async function POST(request: Request) {
|
||||
// Transaktion gelesen: die Reservierung aendert sich nur mit Abos, nicht
|
||||
// durch gleichzeitige Bestellungen — die verbindliche Bestandspruefung
|
||||
// bleibt das bedingte Update in der Transaktion.
|
||||
const reservedByProduct = new Map<string, number>();
|
||||
for (const line of expanded.reserve) {
|
||||
reservedByProduct.set(line.productId, await reservedJarsForProduct(line.productId));
|
||||
}
|
||||
const reservedByProduct = await reservedJarsByProduct(expanded.reserve.map((line) => line.productId));
|
||||
|
||||
// Gutscheincode nur auf den Warenwert anwenden, nicht auf den Versand —
|
||||
// haelt die Rabattlogik einfach nachvollziehbar.
|
||||
// haelt die Rabattlogik einfach nachvollziehbar. Und nur auf Ware
|
||||
// ausserhalb von Patenschaften (discountBase): Schwelle, Mindestwert und
|
||||
// Betrag rechnen ohne Paketpositionen.
|
||||
const base = discountBase(orderItemsData);
|
||||
let discountCents = 0;
|
||||
let discountCode: { id: string; code: string } | null = null;
|
||||
if (input.discountCode) {
|
||||
@@ -347,7 +347,9 @@ export async function POST(request: Request) {
|
||||
where: { code: input.discountCode.trim().toUpperCase() },
|
||||
});
|
||||
|
||||
if (!code || !isDiscountCodeValid(code, subtotalCents, now, totalQuantity)) {
|
||||
// Ein reiner Patenschafts-Warenkorb hat keine Rabattbasis — der Code
|
||||
// gilt dann als nicht einloesbar statt still 0 € abzuziehen.
|
||||
if (!code || base.subtotalCents <= 0 || !isDiscountCodeValid(code, base.subtotalCents, now, base.quantity)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Der Gutscheincode ist ungültig, abgelaufen oder nicht mehr einlösbar." },
|
||||
{ status: 400 }
|
||||
@@ -372,7 +374,7 @@ export async function POST(request: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
discountCents = calculateDiscountCents(code, subtotalCents);
|
||||
discountCents = calculateDiscountCents(code, base.subtotalCents);
|
||||
discountCode = code;
|
||||
} else {
|
||||
// Kein manuell eingegebener Code — unter allen im Admin-Bereich
|
||||
@@ -383,10 +385,10 @@ export async function POST(request: Request) {
|
||||
const candidates = await prisma.discountCode.findMany({ where: { minQuantity: { not: null } } });
|
||||
const now = new Date();
|
||||
const code = candidates
|
||||
.filter((candidate) => isDiscountCodeValid(candidate, subtotalCents, now, totalQuantity))
|
||||
.filter((candidate) => base.subtotalCents > 0 && isDiscountCodeValid(candidate, base.subtotalCents, now, base.quantity))
|
||||
.sort((a, b) => (b.minQuantity ?? 0) - (a.minQuantity ?? 0))[0];
|
||||
if (code) {
|
||||
discountCents = calculateDiscountCents(code, subtotalCents);
|
||||
discountCents = calculateDiscountCents(code, base.subtotalCents);
|
||||
discountCode = code;
|
||||
}
|
||||
}
|
||||
@@ -584,7 +586,8 @@ export async function POST(request: Request) {
|
||||
price_data: {
|
||||
currency: "eur",
|
||||
unit_amount: item.unitPriceCents,
|
||||
product_data: { name: `${item.productName} (${item.weightGrams} g)` },
|
||||
// Gewicht nur bei Ware mit Gewicht — eine Urkunde "(0 g)" waere Unsinn.
|
||||
product_data: { name: item.weightGrams > 0 ? `${item.productName} (${item.weightGrams} g)` : item.productName },
|
||||
},
|
||||
})),
|
||||
...(shippingCents > 0
|
||||
|
||||
Reference in new issue
Block a user