Files
Luncher/server/src/routes/groupRoutes.ts
T
batmanisko 103445dba8
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
feat: limity skupinových objednávek nastavitelné přes env
2026-09-24 09:18:56 +02:00

212 lines
9.4 KiB
TypeScript

import express, { Request } from "express";
import { getLogin } from "../auth";
import { parseToken } from "../utils";
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);
}
router.get("/dates", async (_req, res, next) => {
try {
const dates = await getOrderDates();
res.status(200).json({ dates });
} catch (e: any) { next(e); }
});
router.post("/create", async (req: Request, res, next) => {
const login = getLogin(parseToken(req));
const { name, url } = req.body ?? {};
if (!name || typeof name !== 'string') {
return res.status(400).json({ error: 'Nebyl předán název skupiny' });
}
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 {
const data = await createGroup(login, name, undefined, url);
broadcastExtra(data);
res.status(200).json(data);
} catch (e: any) { next(e); }
});
router.post("/delete", async (req: Request, res, next) => {
const login = getLogin(parseToken(req));
const { id } = req.body ?? {};
if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' });
try {
const data = await deleteGroup(login, id);
broadcastExtra(data);
res.status(200).json(data);
} catch (e: any) { next(e); }
});
router.post("/addMember", async (req: Request, res, next) => {
const login = getLogin(parseToken(req));
const { id, login: targetLogin } = req.body ?? {};
if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' });
if (targetLogin !== undefined && (typeof targetLogin !== 'string' || targetLogin.trim() === '')) {
return res.status(400).json({ error: 'Neplatný login uživatele' });
}
const target = targetLogin ?? login;
try {
const data = await addGroupMember(login, id, target);
broadcastExtra(data);
res.status(200).json(data);
} catch (e: any) { next(e); }
});
router.post("/removeMember", async (req: Request, res, next) => {
const login = getLogin(parseToken(req));
const { id, login: targetLogin } = req.body ?? {};
if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' });
if (!targetLogin) return res.status(400).json({ error: 'Nebyl předán login uživatele' });
try {
const data = await removeGroupMember(login, id, targetLogin);
broadcastExtra(data);
res.status(200).json(data);
} catch (e: any) { next(e); }
});
router.post("/updateMember", async (req: Request, res, next) => {
const login = getLogin(parseToken(req));
const { id, login: targetLogin, amount, note, surchargeText, surchargeAmount } = req.body ?? {};
if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' });
if (!targetLogin) return res.status(400).json({ error: 'Nebyl předán login uživatele' });
const patch: Record<string, any> = {};
if (amount !== undefined) {
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) {
const error = invalidAmount(surchargeAmount, 'Příplatek');
if (error) return res.status(400).json({ error });
patch.surchargeAmount = surchargeAmount;
}
try {
const data = await updateGroupMember(login, id, targetLogin, patch);
broadcastExtra(data);
res.status(200).json(data);
} catch (e: any) { next(e); }
});
router.post("/setState", async (req: Request, res, next) => {
const login = getLogin(parseToken(req));
const { id, state } = req.body ?? {};
if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' });
if (!state || !Object.values(GroupState).includes(state)) {
return res.status(400).json({ error: 'Neplatný stav skupiny' });
}
try {
const data = await setGroupState(login, id, state as GroupState);
broadcastExtra(data);
res.status(200).json(data);
} catch (e: any) { next(e); }
});
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' });
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) {
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);
broadcastExtra(data);
res.status(200).json(data);
} catch (e: any) { next(e); }
});
router.post("/updateTimes", async (req: Request, res, next) => {
const login = getLogin(parseToken(req));
const { id, orderedAt, deliveryAt } = req.body ?? {};
if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' });
const timeRegex = /^([01]\d|2[0-3]):[0-5]\d$/;
if (orderedAt !== undefined && orderedAt !== '' && !timeRegex.test(orderedAt)) {
return res.status(400).json({ error: 'Neplatný formát času objednání (očekáváno HH:MM)' });
}
if (deliveryAt !== undefined && deliveryAt !== '' && !timeRegex.test(deliveryAt)) {
return res.status(400).json({ error: 'Neplatný formát času doručení (očekáváno HH:MM)' });
}
try {
const data = await updateGroupTimes(login, id, orderedAt, deliveryAt);
broadcastExtra(data);
res.status(200).json(data);
} catch (e: any) { next(e); }
});
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' || shareUrl.length > getGroupLimits().maxUrlLength)) {
return res.status(400).json({ error: 'Neplatný odkaz na sledování objednávky' });
}
try {
const data = await setGroupTracking(login, id, shareUrl);
broadcastExtra(data);
res.status(200).json(data);
// Okamžitý poll, ať uživatel nečeká na další tik scheduleru
if (shareUrl) {
checkOrderTracking().catch(e => console.error('Sledování objednávek: okamžitý poll selhal', e));
}
} catch (e: any) { next(e); }
});
export default router;