feat: limity skupinových objednávek nastavitelné přes env
CI / Generate TypeScript types (push) Successful in 12s
CI / Server unit tests (push) Successful in 25s
CI / Build server (push) Successful in 30s
CI / Build client (push) Successful in 39s
CI / Playwright E2E tests (push) Successful in 3m55s
CI / Build and push Docker image (push) Successful in 49s
CI / Notify (push) Successful in 2s

This commit is contained in:
2026-09-24 09:18:56 +02:00
parent 076b8db179
commit 103445dba8
15 changed files with 386 additions and 28 deletions
+44 -18
View File
@@ -5,9 +5,20 @@ import { getWebsocket } from "../websocket";
import { createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState, updateGroupTimes, updateGroupFees, setGroupTracking, getOrderDates } from "../groups";
import { GroupState } from "../../../types/gen/types.gen";
import { checkOrderTracking } from "../orderTracking";
import { getGroupLimits } from "../limits";
const router = express.Router();
// Pojistky proti překlepům (dvojklik, omylem vložený text) — limity viz limits.ts (lze přepsat env)
/** Vrátí chybovou hlášku, pokud částka (v haléřích) není celé číslo v rozsahu 0 až limit. */
function invalidAmount(value: unknown, label: string): string | undefined {
const { maxAmountKc } = getGroupLimits();
if (!Number.isInteger(value) || (value as number) < 0) return `Neplatná výše: ${label}`;
if ((value as number) > maxAmountKc * 100) return `${label} může být nejvýše ${maxAmountKc} Kč`;
return undefined;
}
function broadcastExtra(data: any) {
getWebsocket().emit("message", data);
}
@@ -25,7 +36,11 @@ router.post("/create", async (req: Request, res, next) => {
if (!name || typeof name !== 'string') {
return res.status(400).json({ error: 'Nebyl předán název skupiny' });
}
if (url != null && typeof url !== 'string') {
const limits = getGroupLimits();
if (name.length > limits.maxNameLength) {
return res.status(400).json({ error: `Název skupiny může mít nejvýše ${limits.maxNameLength} znaků` });
}
if (url != null && (typeof url !== 'string' || url.length > limits.maxUrlLength)) {
return res.status(400).json({ error: 'Neplatná URL nabídky' });
}
try {
@@ -80,23 +95,29 @@ router.post("/updateMember", async (req: Request, res, next) => {
if (!targetLogin) return res.status(400).json({ error: 'Nebyl předán login uživatele' });
const patch: Record<string, any> = {};
if (amount !== undefined) {
if (!Number.isInteger(amount) || amount < 0) {
return res.status(400).json({ error: 'Neplatná částka' });
}
const error = invalidAmount(amount, 'Částka');
if (error) return res.status(400).json({ error });
patch.amount = amount;
}
if (note !== undefined) {
if (typeof note !== 'string') return res.status(400).json({ error: 'Neplatná poznámka' });
const { maxNoteLength } = getGroupLimits();
if (note.length > maxNoteLength) {
return res.status(400).json({ error: `Poznámka může mít nejvýše ${maxNoteLength} znaků` });
}
patch.note = note;
}
if (surchargeText !== undefined) {
if (typeof surchargeText !== 'string') return res.status(400).json({ error: 'Neplatný text příplatku' });
const { maxSurchargeTextLength } = getGroupLimits();
if (surchargeText.length > maxSurchargeTextLength) {
return res.status(400).json({ error: `Popis příplatku může mít nejvýše ${maxSurchargeTextLength} znaků` });
}
patch.surchargeText = surchargeText;
}
if (surchargeAmount !== undefined) {
if (!Number.isInteger(surchargeAmount) || surchargeAmount < 0) {
return res.status(400).json({ error: 'Neplatná výše příplatku' });
}
const error = invalidAmount(surchargeAmount, 'Příplatek');
if (error) return res.status(400).json({ error });
patch.surchargeAmount = surchargeAmount;
}
try {
@@ -124,20 +145,25 @@ router.post("/updateFees", async (req: Request, res, next) => {
const login = getLogin(parseToken(req));
const { id, fees, shipping, tip, discountType, discountValue } = req.body ?? {};
if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' });
if (fees !== undefined && (!Number.isInteger(fees) || fees < 0)) {
return res.status(400).json({ error: 'Neplatná výše poplatků' });
}
if (shipping !== undefined && (!Number.isInteger(shipping) || shipping < 0)) {
return res.status(400).json({ error: 'Neplatná výše dopravy' });
}
if (tip !== undefined && (!Number.isInteger(tip) || tip < 0)) {
return res.status(400).json({ error: 'Neplatná výše spropitného' });
for (const [value, label] of [[fees, 'Poplatky'], [shipping, 'Doprava'], [tip, 'Spropitné']] as const) {
const error = value !== undefined ? invalidAmount(value, label) : undefined;
if (error) return res.status(400).json({ error });
}
if (discountType !== undefined && discountType !== '' && !['percent', 'fixed'].includes(discountType)) {
return res.status(400).json({ error: 'Neplatný typ slevy' });
}
if (discountValue !== undefined && (!Number.isInteger(discountValue) || discountValue < 0)) {
return res.status(400).json({ error: 'Neplatná výše slevy' });
if (discountValue !== undefined) {
if (!Number.isInteger(discountValue) || discountValue < 0) {
return res.status(400).json({ error: 'Neplatná výše slevy' });
}
// Procentní sleva nad 100 % by vedla k záporným částkám
if (discountType === 'percent' && discountValue > 100) {
return res.status(400).json({ error: 'Procentní sleva může být nejvýše 100 %' });
}
const { maxAmountKc } = getGroupLimits();
if (discountType !== 'percent' && discountValue > maxAmountKc * 100) {
return res.status(400).json({ error: `Sleva může být nejvýše ${maxAmountKc} Kč` });
}
}
try {
const data = await updateGroupFees(login, id, fees, shipping, tip, discountType, discountValue);
@@ -168,7 +194,7 @@ router.post("/setTracking", async (req: Request, res, next) => {
const login = getLogin(parseToken(req));
const { id, shareUrl } = req.body ?? {};
if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' });
if (shareUrl !== undefined && typeof shareUrl !== 'string') {
if (shareUrl !== undefined && (typeof shareUrl !== 'string' || shareUrl.length > getGroupLimits().maxUrlLength)) {
return res.status(400).json({ error: 'Neplatný odkaz na sledování objednávky' });
}
try {