From 7772db8e63e308e0d1333f06e1b63011a46b9e48 Mon Sep 17 00:00:00 2001 From: Batmanisko Date: Tue, 28 Apr 2026 22:35:15 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=C3=BAhrada=20za=20v=C5=A1echny=20jedno?= =?UTF-8?q?u=20osobou=20(issue=20#29,=20SINGLE=5FPAYMENT)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Přidává možnost, aby jeden strávník zaplatil celý účet v restauraci a ostatní obdrželi QR kód pro refundaci. Prerekvizita — podpora více QR kódů na (příjemce, den): - PendingQr.id (UUID) nahrazuje deduplikaci podle data; každý QR má vlastní klíč - QR obrázky uloženy do Redis/storage (base64) místo tmpdir — přežijí redeploy - GET /api/qr vyžaduje ?id= parametr; dismissQr přijímá {id} místo {date} Feature: - Ikona 'Zaplatit za všechny' v choices-table pro každou LunchChoice (kromě PIZZA/NEOBEDVAM/ROZHODUJI); viditelná jen při ≥2 strávnících a vyplněném účtu - PayForAllModal: tabulka strávníků s prefillovanými cenami z menu, příplatky per-diner, celkové dýško rozpočtené rovnoměrně, generování QR přes POST /api/qr/generate - parsePriceCzk() helper pro parsing 'N Kč' → number Co se nemění: POST /api/qr/generate API kontrakt, PizzaOrder.hasQr boolean Co se mění v OpenAPI: PendingQr.id (required), getPizzaQr ?id param, dismissQr body Co-Authored-By: opmrdkazkrtkaus --- client/src/App.tsx | 54 ++- .../src/components/modals/PayForAllModal.tsx | 308 ++++++++++++++++++ client/src/utils/parsePrice.ts | 11 + server/src/index.ts | 11 +- server/src/pizza.ts | 15 +- server/src/qr.ts | 56 ++-- server/src/routes/pizzaDayRoutes.ts | 6 +- server/src/routes/qrRoutes.ts | 5 +- types/paths/getPizzaQr.yml | 8 +- types/paths/pizzaDay/dismissQr.yml | 8 +- types/schemas/_index.yml | 10 +- 11 files changed, 428 insertions(+), 64 deletions(-) create mode 100644 client/src/components/modals/PayForAllModal.tsx create mode 100644 client/src/utils/parsePrice.ts diff --git a/client/src/App.tsx b/client/src/App.tsx index 1743c5c..29d5220 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -13,12 +13,13 @@ import './App.scss'; import { faCircleCheck, faNoteSticky, faTrashCan, faComment } from '@fortawesome/free-regular-svg-icons'; import { useSettings } from './context/settings'; import Footer from './components/Footer'; -import { faBasketShopping, faChainBroken, faChevronLeft, faChevronRight, faGear, faSatelliteDish, faSearch, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons'; +import { faBasketShopping, faChainBroken, faChevronLeft, faChevronRight, faGear, faMoneyBillTransfer, faSatelliteDish, faSearch, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons'; import Loader from './components/Loader'; import { getHumanDateTime, isInTheFuture, formatDateString } from './Utils'; import NoteModal from './components/modals/NoteModal'; +import PayForAllModal from './components/modals/PayForAllModal'; import { useEasterEgg } from './context/eggs'; -import { ClientData, Food, PizzaOrder, DepartureTime, PizzaDayState, Restaurant, RestaurantDayMenu, RestaurantDayMenuMap, LunchChoice, UserLunchChoice, PizzaVariant, getData, getEasterEggImage, addPizza, removePizza, updatePizzaDayNote, createPizzaDay, deletePizzaDay, lockPizzaDay, unlockPizzaDay, finishOrder, finishDelivery, addChoice, jdemeObed, removeChoices, removeChoice, updateNote, changeDepartureTime, setBuyer, dismissQr } from '../../types'; +import { ClientData, Food, PizzaOrder, DepartureTime, PizzaDayState, Restaurant, RestaurantDayMenu, RestaurantDayMenuMap, LunchChoice, LocationLunchChoicesMap, UserLunchChoice, PizzaVariant, getData, getEasterEggImage, addPizza, removePizza, updatePizzaDayNote, createPizzaDay, deletePizzaDay, lockPizzaDay, unlockPizzaDay, finishOrder, finishDelivery, addChoice, jdemeObed, removeChoices, removeChoice, updateNote, changeDepartureTime, setBuyer, dismissQr, generateQr } from '../../types'; import { getLunchChoiceName } from './enums'; // import FallingLeaves, { LEAF_PRESETS, LEAF_COLOR_THEMES } from './FallingLeaves'; // import './FallingLeaves.scss'; @@ -74,6 +75,7 @@ function App() { const [dayIndex, setDayIndex] = useState(); const [loadingPizzaDay, setLoadingPizzaDay] = useState(false); const [noteModalOpen, setNoteModalOpen] = useState(false); + const [payForAllLocationKey, setPayForAllLocationKey] = useState(null); const [eggImage, setEggImage] = useState(); const eggRef = useRef(null); // Prazvláštní workaround, aby socket.io listener viděl aktuální hodnotu @@ -615,6 +617,18 @@ function App() { {locationName} {(locationPickCount ?? 0) > 1 && ({locationPickCount})} + {locationPickCount >= 2 && auth.login && loginObject[auth.login] !== undefined + && locationKey !== LunchChoice.PIZZA && locationKey !== LunchChoice.NEOBEDVAM && locationKey !== LunchChoice.ROZHODUJI + && settings?.bankAccount && settings?.holderName && ( + + setPayForAllLocationKey(locationKey)} + className='action-icon' + style={{ cursor: 'pointer' }} + /> + + )} @@ -807,11 +821,15 @@ function App() { } { - data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr && -
-

QR platba

- QR kód -
+ data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr && (() => { + const pizzaQr = data.pendingQrs?.find(qr => qr.creator === data.pizzaDay?.creator); + return pizzaQr ? ( +
+

QR platba

+ QR kód +
+ ) : null; + })() } } @@ -821,18 +839,17 @@ function App() { {data.pendingQrs && data.pendingQrs.length > 0 &&

Nevyřízené platby

-

Máte neuhrazené platby z předchozích dní.

+

Máte neuhrazené platby.

{data.pendingQrs.map(qr => ( -
+

{formatDateString(qr.date)} — {qr.creator} ({qr.totalPrice} Kč) {qr.purpose && <>
{qr.purpose}}

- QR kód + QR kód
); } diff --git a/client/src/components/modals/PayForAllModal.tsx b/client/src/components/modals/PayForAllModal.tsx new file mode 100644 index 0000000..e1b65bb --- /dev/null +++ b/client/src/components/modals/PayForAllModal.tsx @@ -0,0 +1,308 @@ +import { useState, useEffect, useCallback } from "react"; +import { Modal, Button, Form, Table, Alert } from "react-bootstrap"; +import { generateQr, LunchChoice, LocationLunchChoicesMap, RestaurantDayMenu, QrRecipient } from "../../../../types"; +import { parsePriceCzk } from "../../utils/parsePrice"; + +type DinerEntry = { + login: string; + selectedFoods: number[]; + baseAmount: number; + baseAmountParseFailed: boolean; + surchargeText: string; + surchargeAmount: string; + included: boolean; +}; + +type Props = { + isOpen: boolean; + onClose: () => void; + locationKey: LunchChoice; + locationName: string; + locationChoices: LocationLunchChoicesMap; + menu: RestaurantDayMenu | undefined; + payerLogin: string; + bankAccount: string; + bankAccountHolder: string; +}; + +function sanitizeAmount(value: string): string { + return value.replace(/[^0-9.,]/g, '').replace(',', '.'); +} + +function parseAmount(s: string): number | null { + if (!s || s.trim().length === 0) return null; + const n = parseFloat(s); + if (isNaN(n) || n < 0) return null; + const parts = s.split('.'); + if (parts.length === 2 && parts[1].length > 2) return null; + return Math.round(n * 100) / 100; +} + +export default function PayForAllModal({ isOpen, onClose, locationName, locationChoices, menu, payerLogin, bankAccount, bankAccountHolder }: Readonly) { + const [diners, setDiners] = useState([]); + const [tipTotal, setTipTotal] = useState(''); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(false); + + const hasMenu = !!menu; + + useEffect(() => { + if (!isOpen) return; + const entries: DinerEntry[] = Object.entries(locationChoices).map(([login, choice]) => { + const selectedFoods = choice.selectedFoods ?? []; + let baseAmount = 0; + let baseAmountParseFailed = false; + if (menu) { + for (const idx of selectedFoods) { + const price = parsePriceCzk(menu.food?.[idx]?.price); + if (price === null) { + baseAmountParseFailed = true; + } else { + baseAmount += price; + } + } + } + return { + login, + selectedFoods, + baseAmount, + baseAmountParseFailed, + surchargeText: '', + surchargeAmount: '', + included: login !== payerLogin, + }; + }); + setDiners(entries); + setTipTotal(''); + setError(null); + setSuccess(false); + }, [isOpen, locationChoices, menu, payerLogin]); + + const includedDiners = diners.filter(d => d.included && d.login !== payerLogin); + const tipPerPerson = (() => { + if (includedDiners.length === 0) return 0; + const tip = parseAmount(tipTotal); + if (tip === null || tip === 0) return 0; + return Math.round((tip / includedDiners.length) * 100) / 100; + })(); + + const getTotal = (d: DinerEntry): number => { + const surcharge = parseAmount(d.surchargeAmount) ?? 0; + const tip = d.included && d.login !== payerLogin ? tipPerPerson : 0; + return Math.round((d.baseAmount + surcharge + tip) * 100) / 100; + }; + + const handleInclude = useCallback((login: string, checked: boolean) => { + setDiners(prev => prev.map(d => d.login === login ? { ...d, included: checked } : d)); + }, []); + + const handleSurchargeText = useCallback((login: string, value: string) => { + setDiners(prev => prev.map(d => d.login === login ? { ...d, surchargeText: value } : d)); + }, []); + + const handleSurchargeAmount = useCallback((login: string, value: string) => { + setDiners(prev => prev.map(d => d.login === login ? { ...d, surchargeAmount: sanitizeAmount(value) } : d)); + }, []); + + const handleGenerate = async () => { + setError(null); + const recipients: QrRecipient[] = []; + + for (const d of diners) { + if (!d.included || d.login === payerLogin) continue; + const total = getTotal(d); + if (total <= 0) { + setError(`Celková částka pro ${d.login} musí být kladná`); + return; + } + const amountStr = total.toString(); + if (amountStr.includes('.') && amountStr.split('.')[1].length > 2) { + setError(`Částka pro ${d.login} má více než 2 desetinná místa`); + return; + } + const foods = d.selectedFoods.map(i => menu?.food?.[i]?.name).filter(Boolean).join(', '); + const purposeBase = `Oběd ${locationName}${foods ? ` — ${foods}` : ''}`; + recipients.push({ + login: d.login, + purpose: purposeBase.substring(0, 60), + amount: total, + }); + } + + if (recipients.length === 0) { + setError("Nebyl vybrán žádný příjemce"); + return; + } + + setLoading(true); + try { + const response = await generateQr({ + body: { recipients, bankAccount, bankAccountHolder }, + }); + if (response.error) { + setError((response.error as any).error || 'Nastala chyba při generování QR kódů'); + } else { + setSuccess(true); + setTimeout(() => onClose(), 2000); + } + } catch (e: any) { + setError(e.message || 'Nastala chyba při generování QR kódů'); + } finally { + setLoading(false); + } + }; + + const anyParseFailed = diners.some(d => d.baseAmountParseFailed && d.included); + + return ( + + +

Zaplatit za všechny — {locationName}

+
+ + {success ? ( + + QR kódy byly úspěšně vygenerovány! Uživatelé je uvidí v sekci „Nevyřízené platby". + + ) : ( + <> +

Zaplatili jste za skupinu v restauraci. Nastavte příplatky a dýško, poté vygenerujte QR kódy pro ostatní.

+ + {!hasMenu && ( + + Pro tuto skupinu nejsou k dispozici ceny jídel — vyplňte příplatky ručně. + + )} + + {anyParseFailed && ( + + U některých jídel se nepodařilo načíst cenu — doplňte ji ručně v sloupci Příplatek. + + )} + + {error && ( + setError(null)} dismissible> + {error} + + )} + +
+ + + + + + + + + + + + {diners.map(d => { + const isPayer = d.login === payerLogin; + const foodNames = d.selectedFoods.map(i => menu?.food?.[i]?.name).filter(Boolean).join(', '); + const total = getTotal(d); + return ( + + + + + + + + + ); + })} + +
StrávníkJídlaPříplatekDýškoCelkem
+ {isPayer ? ( + plátce + ) : ( + handleInclude(d.login, e.target.checked)} + /> + )} + {d.login} + + {foodNames || } + {hasMenu && d.baseAmount > 0 && ({d.baseAmount} Kč)} + {d.baseAmountParseFailed && } + + + {!isPayer && ( +
+ handleSurchargeText(d.login, e.target.value)} + disabled={!d.included} + size="sm" + onKeyDown={e => e.stopPropagation()} + /> + handleSurchargeAmount(d.login, e.target.value)} + disabled={!d.included} + size="sm" + style={{ width: 70 }} + onKeyDown={e => e.stopPropagation()} + /> +
+ )} +
+ {!isPayer && d.included ? `${tipPerPerson} Kč` : '—'} + + {!isPayer ? `${total} Kč` : '—'} +
+ +
+ + setTipTotal(sanitizeAmount(e.target.value))} + size="sm" + style={{ width: 100 }} + onKeyDown={e => e.stopPropagation()} + /> + + {includedDiners.length > 0 && tipPerPerson > 0 + ? `(${tipPerPerson} Kč / osoba)` + : ''} + +
+ + )} + + + {!success && ( + <> + + Příjemci: {includedDiners.length} + + + + + )} + {success && ( + + )} + + + ); +} diff --git a/client/src/utils/parsePrice.ts b/client/src/utils/parsePrice.ts new file mode 100644 index 0000000..4863b37 --- /dev/null +++ b/client/src/utils/parsePrice.ts @@ -0,0 +1,11 @@ +/** + * Parsuje cenu ve formátu "135 Kč", "135,50 Kč" nebo "135.50 Kč" na číslo. + * Vrátí null při selhání. + */ +export function parsePriceCzk(raw: string | undefined): number | null { + if (!raw) return null; + const m = raw.replace(',', '.').match(/(\d+(?:\.\d+)?)/); + if (!m) return null; + const n = parseFloat(m[1]); + return Number.isFinite(n) ? n : null; +} diff --git a/server/src/index.ts b/server/src/index.ts index 384eb5f..7dd9f89 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -86,12 +86,15 @@ app.post("/api/login", (req, res) => { } }); -// TODO dočasné řešení - QR se zobrazuje přes , nemáme sem jak dostat token -app.get("/api/qr", (req, res) => { +// QR se zobrazuje přes , nemáme sem jak dostat token +app.get("/api/qr", async (req, res) => { if (!req.query?.login) { - throw Error("Nebyl předán login"); + return res.status(400).json({ error: "Nebyl předán login" }); } - const img = getQr(req.query.login as string); + if (!req.query?.id) { + return res.status(400).json({ error: "Nebyl předán identifikátor QR kódu" }); + } + const img = await getQr(req.query.login as string, req.query.id as string); res.writeHead(200, { 'Content-Type': 'image/png', 'Content-Length': img.length diff --git a/server/src/pizza.ts b/server/src/pizza.ts index a9434c8..f20eaab 100644 --- a/server/src/pizza.ts +++ b/server/src/pizza.ts @@ -5,6 +5,7 @@ import getStorage from "./storage"; import { downloadPizzy } from "./chefie"; import { getClientData, getToday, initIfNeeded } from "./service"; import { Pizza, ClientData, PizzaDayState, PizzaSize, PizzaOrder, PizzaVariant, UdalostEnum, PendingQr } from "../../types/gen/types.gen"; +import crypto from "crypto"; const storage = getStorage(); const PENDING_QR_PREFIX = 'pending_qr'; @@ -269,11 +270,13 @@ export async function finishPizzaDelivery(login: string, bankAccount?: string, b if (bankAccount?.length && bankAccountHolder?.length) { for (const order of clientData.pizzaDay.orders!) { if (order.customer !== login) { // zatím platí creator = objednávající, a pro toho nemá QR kód smysl + const id = crypto.randomUUID(); let message = order.pizzaList!.map(pizza => `Pizza ${pizza.name} (${pizza.size})`).join(', '); - await generateQr(order.customer, bankAccount, bankAccountHolder, order.totalPrice, message); + await generateQr(order.customer, bankAccount, bankAccountHolder, order.totalPrice, message, id); order.hasQr = true; // Uložíme nevyřízený QR kód pro persistentní zobrazení await addPendingQr(order.customer, { + id, date: today, creator: login, totalPrice: order.totalPrice, @@ -360,8 +363,8 @@ function getPendingQrKey(login: string): string { export async function addPendingQr(login: string, pendingQr: PendingQr): Promise { const key = getPendingQrKey(login); const existing = await storage.getData(key) ?? []; - // Nepřidáváme duplicity pro stejný den - if (!existing.some(qr => qr.date === pendingQr.date)) { + // Deduplikace podle id (ne podle data — jeden den může mít uživatel víc QR kódů) + if (!existing.some(qr => qr.id === pendingQr.id)) { existing.push(pendingQr); await storage.setData(key, existing); } @@ -375,11 +378,11 @@ export async function getPendingQrs(login: string): Promise { } /** - * Označí QR kód pro daný den jako uhrazený (odstraní ho ze seznamu nevyřízených). + * Označí QR kód jako uhrazený (odstraní ho ze seznamu nevyřízených). */ -export async function dismissPendingQr(login: string, date: string): Promise { +export async function dismissPendingQr(login: string, id: string): Promise { const key = getPendingQrKey(login); const existing = await storage.getData(key) ?? []; - const filtered = existing.filter(qr => qr.date !== date); + const filtered = existing.filter(qr => qr.id !== id); await storage.setData(key, filtered); } \ No newline at end of file diff --git a/server/src/qr.ts b/server/src/qr.ts index 2493bc1..b9a5507 100644 --- a/server/src/qr.ts +++ b/server/src/qr.ts @@ -1,19 +1,17 @@ -import fs from "fs"; import axios from "axios"; -import os from "os"; -import path from "path"; import crypto from "crypto"; -import { formatDate } from "./utils"; +import getStorage from "./storage"; const QR_GENERATOR_URL = 'https://api.paylibo.com/paylibo/generator/image'; const COUNTRY_CODE = 'CZ'; const CURRENCY_CODE = 'CZK'; const QR_PIXEL_SIZE = 256; -const tmpDir = os.tmpdir(); + +const storage = getStorage(); /** * Převede číslo účtu z BBAN do IBAN. Automaticky dopočítá kontrolní číslice. - * + * * @param bankAccountNumber číslo účtu ve formátu BBAN (123456-0123456789/0100) */ function convertBbanToIban(bankAccountNumber: string): string { @@ -41,26 +39,23 @@ function convertBbanToIban(bankAccountNumber: string): string { return iban; } -function createNameHash(customerName: string): string { - return crypto.createHash('md5').update(customerName).digest('hex'); -} - -function createFilePath(nameHash: string): string { - const fileName = `${formatDate(new Date())}_${nameHash}.png`; - return path.join(tmpDir, fileName); +function createStorageKey(customerName: string, id: string): string { + const nameHash = crypto.createHash('md5').update(customerName).digest('hex'); + return `qr_${nameHash}_${id}`; } /** - * Vygeneruje, uloží a vrátí unikátní ID obrázku platebního QR kódu s danými parametry. - * + * Vygeneruje a uloží obrázek platebního QR kódu do storage (Redis/JSON). + * Data přežijí redeploy — není třeba persistentní filesystém. + * * @param customerName jméno uživatele, pro kterého je QR kód generován * @param bankAccountNumber číslo cílového bankovního účtu ve formátu BBAN * @param bankAccountHolder jméno držitele cílového bankovního účtu * @param amount částka v Kč * @param message zpráva pro příjemce - * @returns hash, pomocí kterého lze následně získat vygenerovaný obrázek + * @param id unikátní identifikátor (UUID) tohoto QR kódu */ -export async function generateQr(customerName: string, bankAccountNumber: string, bankAccountHolder: string, amount: number, message: string): Promise { +export async function generateQr(customerName: string, bankAccountNumber: string, bankAccountHolder: string, amount: number, message: string, id: string): Promise { // Zpráva pro příjemce nesmí dle standardu obsahovat '*' a být delší než 60 znaků if (message.indexOf('*') >= 0) { message = message.replace('*', ''); @@ -77,22 +72,23 @@ export async function generateQr(customerName: string, bankAccountNumber: string branding: false, compress: false, size: QR_PIXEL_SIZE, - } - const response = await axios.get(QR_GENERATOR_URL, { responseType: 'stream', params: { ...payload } }); - // Použijeme hash, abychom nemuseli řešit nepovolené znaky ve jménu uživatele - const nameHash = createNameHash(customerName); - const imgPath = createFilePath(nameHash); - response.data.pipe(fs.createWriteStream(imgPath)); - return nameHash; + }; + const response = await axios.get(QR_GENERATOR_URL, { responseType: 'arraybuffer', params: { ...payload } }); + const base64 = Buffer.from(response.data).toString('base64'); + await storage.setData(createStorageKey(customerName, id), base64); } /** - * Vrátí obrázek s QR kódem, pokud existuje. - * + * Vrátí obrázek s QR kódem ze storage. + * * @param customerName jméno uživatele + * @param id unikátní identifikátor QR kódu * @returns data obrázku */ -export function getQr(customerName: string): Buffer { - const imgPath = createFilePath(createNameHash(customerName)); - return fs.readFileSync(imgPath); -} \ No newline at end of file +export async function getQr(customerName: string, id: string): Promise { + const base64 = await storage.getData(createStorageKey(customerName, id)); + if (!base64) { + throw new Error("QR kód nebyl nalezen"); + } + return Buffer.from(base64, 'base64'); +} diff --git a/server/src/routes/pizzaDayRoutes.ts b/server/src/routes/pizzaDayRoutes.ts index 82a26c5..83601b6 100644 --- a/server/src/routes/pizzaDayRoutes.ts +++ b/server/src/routes/pizzaDayRoutes.ts @@ -112,11 +112,11 @@ router.post("/updatePizzaFee", async (req: Request<{}, any, UpdatePizzaFeeData[" /** Označí QR kód jako uhrazený. */ router.post("/dismissQr", async (req: Request<{}, any, DismissQrData["body"]>, res, next) => { const login = getLogin(parseToken(req)); - if (!req.body.date) { - return res.status(400).json({ error: "Nebyl předán datum" }); + if (!req.body.id) { + return res.status(400).json({ error: "Nebyl předán identifikátor QR kódu" }); } try { - await dismissPendingQr(login, req.body.date); + await dismissPendingQr(login, req.body.id); res.status(200).json({}); } catch (e: any) { next(e) } }); diff --git a/server/src/routes/qrRoutes.ts b/server/src/routes/qrRoutes.ts index e89a217..e00a69c 100644 --- a/server/src/routes/qrRoutes.ts +++ b/server/src/routes/qrRoutes.ts @@ -4,6 +4,7 @@ import { parseToken, formatDate } from "../utils"; import { generateQr } from "../qr"; import { addPendingQr } from "../pizza"; import { GenerateQrData } from "../../../types"; +import crypto from "crypto"; const router = express.Router(); @@ -44,10 +45,12 @@ router.post("/generate", async (req: Request<{}, any, GenerateQrData["body"]>, r } // Vygenerovat QR kód - await generateQr(recipient.login, bankAccount, bankAccountHolder, recipient.amount, recipient.purpose); + const id = crypto.randomUUID(); + await generateQr(recipient.login, bankAccount, bankAccountHolder, recipient.amount, recipient.purpose, id); // Uložit jako nevyřízený QR kód await addPendingQr(recipient.login, { + id, date: today, creator: login, totalPrice: recipient.amount, diff --git a/types/paths/getPizzaQr.yml b/types/paths/getPizzaQr.yml index ba7ba49..81e69dc 100644 --- a/types/paths/getPizzaQr.yml +++ b/types/paths/getPizzaQr.yml @@ -1,6 +1,6 @@ get: operationId: getPizzaQr - summary: Získání QR kódu pro platbu za Pizza day + summary: Získání QR kódu pro platbu security: [] # Nevyžaduje autentizaci parameters: - in: query @@ -9,6 +9,12 @@ get: type: string required: true description: Přihlašovací jméno uživatele, pro kterého bude vrácen QR kód + - in: query + name: id + schema: + type: string + required: true + description: Unikátní identifikátor QR kódu (z PendingQr.id) responses: "200": description: Vygenerovaný QR kód pro platbu diff --git a/types/paths/pizzaDay/dismissQr.yml b/types/paths/pizzaDay/dismissQr.yml index ffa95bb..0aa6fba 100644 --- a/types/paths/pizzaDay/dismissQr.yml +++ b/types/paths/pizzaDay/dismissQr.yml @@ -1,17 +1,17 @@ post: operationId: dismissQr - summary: Označí QR kód pro daný den jako uhrazený (odstraní ho ze seznamu nevyřízených). + summary: Označí QR kód jako uhrazený (odstraní ho ze seznamu nevyřízených). requestBody: required: true content: application/json: schema: properties: - date: - description: Datum Pizza day, ke kterému se QR kód vztahuje + id: + description: Unikátní identifikátor QR kódu (z PendingQr.id) type: string required: - - date + - id responses: "200": description: QR kód byl označen jako uhrazený. diff --git a/types/schemas/_index.yml b/types/schemas/_index.yml index 75915d4..560217c 100644 --- a/types/schemas/_index.yml +++ b/types/schemas/_index.yml @@ -632,19 +632,23 @@ ClearMockDataRequest: # --- NEVYŘÍZENÉ QR KÓDY --- PendingQr: - description: Nevyřízený QR kód pro platbu z předchozího Pizza day + description: Nevyřízený QR kód pro platbu type: object additionalProperties: false required: + - id - date - creator - totalPrice properties: + id: + description: Unikátní identifikátor QR kódu (umožňuje více QR na strávníka na den) + type: string date: - description: Datum Pizza day, ke kterému se QR kód vztahuje + description: Datum, ke kterému se QR kód vztahuje type: string creator: - description: Jméno zakladatele Pizza day (objednávajícího) + description: Jméno uživatele, který QR vygeneroval (příjemce platby) type: string totalPrice: description: Celková cena objednávky v Kč