fix: počítání částek v haléřích z důvodu přesnosti
CI / Generate TypeScript types (push) Successful in 21s
CI / Build server (push) Successful in 25s
CI / Server unit tests (push) Successful in 55s
CI / Build client (push) Successful in 33s
CI / Playwright E2E tests (push) Successful in 1m20s
CI / Build and push Docker image (push) Successful in 35s
CI / Notify (push) Successful in 2s

This commit is contained in:
2026-05-07 13:05:04 +02:00
parent d91c8db49c
commit 8aef00ab05
18 changed files with 370 additions and 380 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ export default function PizzaOrderList({ state, orders, onDelete, creator }: Rea
borderTop: '2px solid var(--luncher-border)'
}}>
<td colSpan={4} style={{ padding: '16px 20px', border: 'none' }}>Celkem</td>
<td style={{ padding: '16px 20px', border: 'none', textAlign: 'right', color: 'var(--luncher-primary)' }}>{`${total}`}</td>
<td style={{ padding: '16px 20px', border: 'none', textAlign: 'right', color: 'var(--luncher-primary)' }}>{`${total / 100}`}</td>
</tr>
</tbody>
</Table>
+4 -4
View File
@@ -26,7 +26,7 @@ export default function PizzaOrderRow({ creator, order, state, onDelete, onFeeMo
<td>{order.customer}</td>
<td>{order.pizzaList!.map<React.ReactNode>(pizzaOrder =>
<span key={pizzaOrder.name}>
{`${pizzaOrder.name}, ${pizzaOrder.size} (${pizzaOrder.price} Kč)`}
{`${pizzaOrder.name}, ${pizzaOrder.size} (${pizzaOrder.price / 100} Kč)`}
{auth?.login === order.customer && state === PizzaDayState.CREATED &&
<span title='Odstranit'>
<FontAwesomeIcon onClick={() => {
@@ -38,10 +38,10 @@ export default function PizzaOrderRow({ creator, order, state, onDelete, onFeeMo
.reduce((prev, curr, index) => [prev, <br key={`br-${index}`} />, curr])}
</td>
<td style={{ maxWidth: "200px" }}>{order.note ?? '-'}</td>
<td style={{ maxWidth: "200px" }}>{order.fee?.price ? `${order.fee.price}${order.fee.text ? ` (${order.fee.text})` : ''}` : '-'}</td>
<td style={{ maxWidth: "200px" }}>{order.fee?.price ? `${order.fee.price / 100}${order.fee.text ? ` (${order.fee.text})` : ''}` : '-'}</td>
<td>
{order.totalPrice} {auth?.login === creator && state === PizzaDayState.CREATED && <span title='Nastavit příplatek'><FontAwesomeIcon onClick={() => { setIsFeeModalOpen(true) }} className='action-icon' icon={faMoneyBill1} /></span>}
{order.totalPrice / 100} {auth?.login === creator && state === PizzaDayState.CREATED && <span title='Nastavit příplatek'><FontAwesomeIcon onClick={() => { setIsFeeModalOpen(true) }} className='action-icon' icon={faMoneyBill1} /></span>}
</td>
<PizzaAdditionalFeeModal customerName={order.customer} isOpen={isFeeModalOpen} onClose={() => setIsFeeModalOpen(false)} onSave={saveFees} initialValues={{ text: order.fee?.text, price: order.fee?.price?.toString() }} />
<PizzaAdditionalFeeModal customerName={order.customer} isOpen={isFeeModalOpen} onClose={() => setIsFeeModalOpen(false)} onSave={saveFees} initialValues={{ text: order.fee?.text, price: order.fee?.price != null ? String(order.fee.price / 100) : undefined }} />
</>
}
@@ -9,18 +9,23 @@ type Props = {
onSaved: (data: any) => void;
};
function parseNum(s: string): number {
function parseHal(s: string): number {
const n = parseFloat(s.replace(',', '.'));
return isNaN(n) || n < 0 ? 0 : Math.round(n * 100) / 100;
return isNaN(n) || n < 0 ? 0 : Math.round(n * 100);
}
function parsePercent(s: string): number {
const n = parseFloat(s.replace(',', '.'));
return isNaN(n) || n < 0 ? 0 : Math.round(n);
}
function computeMemberTotal(member: OrderGroupMember, feeShare: number, discountType: string, discountValue: number, memberCount: number): number {
const base = member.amount ?? 0;
const surcharge = member.surchargeAmount ?? 0;
const discount = discountType === 'percent'
? Math.round((base + surcharge) * discountValue / 100 * 100) / 100
: Math.round(discountValue / memberCount * 100) / 100;
return Math.round((base + surcharge + feeShare - discount) * 100) / 100;
? Math.round((base + surcharge) * discountValue / 100)
: Math.round(discountValue / memberCount);
return base + surcharge + feeShare - discount;
}
export default function EditGroupFeesModal({ isOpen, onClose, group, onSaved }: Readonly<Props>) {
@@ -34,23 +39,25 @@ export default function EditGroupFeesModal({ isOpen, onClose, group, onSaved }:
useEffect(() => {
if (!isOpen) return;
setFees(group.fees ? String(group.fees) : '');
setShipping(group.shipping ? String(group.shipping) : '');
setTip(group.tip ? String(group.tip) : '');
setFees(group.fees ? String(group.fees / 100) : '');
setShipping(group.shipping ? String(group.shipping / 100) : '');
setTip(group.tip ? String(group.tip / 100) : '');
setDiscountType((group.discountType as 'percent' | 'fixed') ?? 'percent');
setDiscountValue(group.discountValue ? String(group.discountValue) : '');
setDiscountValue(group.discountValue
? ((group.discountType as string) === 'fixed' ? String(group.discountValue / 100) : String(group.discountValue))
: '');
setError(null);
}, [isOpen, group]);
const memberEntries = Object.entries(group.members) as [string, OrderGroupMember][];
const memberCount = memberEntries.length;
const feesNum = parseNum(fees);
const shippingNum = parseNum(shipping);
const tipNum = parseNum(tip);
const discountNum = parseNum(discountValue);
const feesNum = parseHal(fees);
const shippingNum = parseHal(shipping);
const tipNum = parseHal(tip);
const discountNum = discountType === 'percent' ? parsePercent(discountValue) : parseHal(discountValue);
const totalFees = feesNum + shippingNum + tipNum;
const feeShare = memberCount > 0 ? Math.round(totalFees / memberCount * 100) / 100 : 0;
const feeShare = memberCount > 0 ? Math.round(totalFees / memberCount) : 0;
const handleSave = async () => {
setError(null);
@@ -64,7 +71,7 @@ export default function EditGroupFeesModal({ isOpen, onClose, group, onSaved }:
tip: tipNum,
discountType: discountNum > 0 ? discountType : undefined,
discountValue: discountNum > 0 ? discountNum : undefined,
},
}
});
if (res.error) {
setError((res.error as any).error || 'Nastala chyba');
@@ -143,7 +150,7 @@ export default function EditGroupFeesModal({ isOpen, onClose, group, onSaved }:
</div>
<hr />
<h6>Náhled celkových částek ({memberCount} členů, {feeShare > 0 ? `poplatek ${feeShare} Kč/os.` : 'bez poplatku'})</h6>
<h6>Náhled celkových částek ({memberCount} členů, {feeShare > 0 ? `poplatek ${feeShare / 100} Kč/os.` : 'bez poplatku'})</h6>
<Table size="sm" bordered>
<thead>
<tr>
@@ -161,18 +168,18 @@ export default function EditGroupFeesModal({ isOpen, onClose, group, onSaved }:
const surcharge = member.surchargeAmount ?? 0;
const discount = discountNum > 0
? (discountType === 'percent'
? Math.round((base + surcharge) * discountNum / 100 * 100) / 100
: Math.round(discountNum / memberCount * 100) / 100)
? Math.round((base + surcharge) * discountNum / 100)
: Math.round(discountNum / memberCount))
: 0;
const total = computeMemberTotal(member, feeShare, discountType, discountNum, memberCount);
return (
<tr key={login}>
<td><strong>{login}</strong></td>
<td className="text-end">{base > 0 ? `${base}` : '—'}</td>
<td className="text-end">{surcharge > 0 ? `${surcharge}` : '—'}</td>
<td className="text-end">{feeShare > 0 ? `${feeShare}` : '—'}</td>
<td className="text-end text-danger">{discount > 0 ? `-${discount}` : '—'}</td>
<td className="text-end fw-bold">{total > 0 ? `${total}` : '—'}</td>
<td className="text-end">{base > 0 ? `${base / 100}` : '—'}</td>
<td className="text-end">{surcharge > 0 ? `${surcharge / 100}` : '—'}</td>
<td className="text-end">{feeShare > 0 ? `${feeShare / 100}` : '—'}</td>
<td className="text-end text-danger">{discount > 0 ? `-${discount / 100}` : '—'}</td>
<td className="text-end fw-bold">{total > 0 ? `${total / 100}` : '—'}</td>
</tr>
);
})}
+12 -19
View File
@@ -33,9 +33,7 @@ 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;
return Math.round(n * 100);
}
export default function PayForAllModal({ isOpen, onClose, locationName, locationChoices, menu, payerLogin, bankAccount, bankAccountHolder }: Readonly<Props>) {
@@ -55,11 +53,11 @@ export default function PayForAllModal({ isOpen, onClose, locationName, location
let baseAmountParseFailed = false;
if (menu) {
for (const idx of selectedFoods) {
const price = parsePriceCzk(menu.food?.[idx]?.price);
if (price === null) {
const priceKc = parsePriceCzk(menu.food?.[idx]?.price);
if (priceKc === null) {
baseAmountParseFailed = true;
} else {
baseAmount += price;
baseAmount += Math.round(priceKc * 100);
}
}
}
@@ -84,19 +82,19 @@ export default function PayForAllModal({ isOpen, onClose, locationName, location
if (includedDiners.length === 0) return 0;
const tip = parseAmount(tipTotal);
if (tip === null || tip === 0) return 0;
const totalPeople = includedDiners.length + 1; // +1 for payer
return Math.round((tip / totalPeople) * 100) / 100;
const totalPeople = includedDiners.length + 1;
return Math.round(tip / totalPeople);
})();
const payerTipShare = (() => {
const tip = parseAmount(tipTotal);
if (!tip) return 0;
return Math.round((tip - tipPerPerson * includedDiners.length) * 100) / 100;
return tip - tipPerPerson * includedDiners.length;
})();
const getTotal = (d: DinerEntry): number => {
const surcharge = parseAmount(d.surchargeAmount) ?? 0;
const tip = d.login === payerLogin ? payerTipShare : tipPerPerson;
return Math.round((d.baseAmount + surcharge + tip) * 100) / 100;
return d.baseAmount + surcharge + tip;
};
const handleInclude = useCallback((login: string, checked: boolean) => {
@@ -122,11 +120,6 @@ export default function PayForAllModal({ isOpen, onClose, locationName, location
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({
@@ -226,7 +219,7 @@ export default function PayForAllModal({ isOpen, onClose, locationName, location
<td>
<small>
{foodNames || <span className="text-muted">—</span>}
{hasMenu && d.baseAmount > 0 && <span className="text-muted"> ({d.baseAmount} Kč)</span>}
{hasMenu && d.baseAmount > 0 && <span className="text-muted"> ({d.baseAmount / 100} Kč)</span>}
{d.baseAmountParseFailed && <span className="text-warning"> ⚠</span>}
</small>
</td>
@@ -254,10 +247,10 @@ export default function PayForAllModal({ isOpen, onClose, locationName, location
</div>
</td>
<td className="text-end">
{(() => { const s = isPayer ? payerTipShare : tipPerPerson; return s > 0 ? `${s} Kč` : '—'; })()}
{(() => { const s = isPayer ? payerTipShare : tipPerPerson; return s > 0 ? `${s / 100} Kč` : '—'; })()}
</td>
<td className="text-end fw-bold">
{`${total} Kč`}
{`${total / 100} Kč`}
</td>
</tr>
);
@@ -278,7 +271,7 @@ export default function PayForAllModal({ isOpen, onClose, locationName, location
/>
<small className="text-muted">
{includedDiners.length > 0 && tipPerPerson > 0
? `(${tipPerPerson} Kč / osoba)`
? `(${tipPerPerson / 100} Kč / osoba)`
: ''}
</small>
</div>
@@ -41,7 +41,7 @@ export default function PayForGroupModal({ isOpen, onClose, group, payerLogin, b
const shipping = group.shipping ?? 0;
const tip = group.tip ?? 0;
const totalFees = fees + shipping + tip;
const feeShare = memberCount > 0 ? Math.round(totalFees / memberCount * 100) / 100 : 0;
const feeShare = memberCount > 0 ? Math.round(totalFees / memberCount) : 0;
const getMemberTotal = (entry: DinerEntry): number => {
const base = entry.member.amount ?? 0;
@@ -50,10 +50,10 @@ export default function PayForGroupModal({ isOpen, onClose, group, payerLogin, b
const discountValue = group.discountValue ?? 0;
const discount = discountValue > 0
? (discountType === 'percent'
? Math.round((base + surcharge) * discountValue / 100 * 100) / 100
: Math.round(discountValue / memberCount * 100) / 100)
? Math.round((base + surcharge) * discountValue / 100)
: Math.round(discountValue / memberCount))
: 0;
return Math.round((base + surcharge + feeShare - discount) * 100) / 100;
return base + surcharge + feeShare - discount;
};
const includedNonPayers = diners.filter(d => d.included && d.login !== payerLogin);
@@ -73,11 +73,6 @@ export default function PayForGroupModal({ isOpen, onClose, group, payerLogin, b
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;
}
recipients.push({
login: d.login,
purpose: `Objednávka ${group.name}`.substring(0, 60),
@@ -132,15 +127,15 @@ export default function PayForGroupModal({ isOpen, onClose, group, payerLogin, b
{hasFees && (
<div className="d-flex gap-3 mb-2 text-muted" style={{ fontSize: '0.9em' }}>
{fees > 0 && <span>Poplatky: <strong>{fees} Kč</strong></span>}
{shipping > 0 && <span>Doprava: <strong>{shipping} Kč</strong></span>}
{tip > 0 && <span>Spropitné: <strong>{tip} Kč</strong></span>}
<span>→ {feeShare} Kč/os.</span>
{fees > 0 && <span>Poplatky: <strong>{fees / 100} Kč</strong></span>}
{shipping > 0 && <span>Doprava: <strong>{shipping / 100} Kč</strong></span>}
{tip > 0 && <span>Spropitné: <strong>{tip / 100} Kč</strong></span>}
<span>→ {feeShare / 100} Kč/os.</span>
</div>
)}
{group.discountValue != null && group.discountValue > 0 && (
<div className="mb-2 text-success" style={{ fontSize: '0.9em' }}>
Sleva: {group.discountType === 'percent' ? `${group.discountValue}%` : `${group.discountValue} Kč`}
Sleva: {group.discountType === 'percent' ? `${group.discountValue}%` : `${group.discountValue / 100} Kč`}
</div>
)}
@@ -180,18 +175,18 @@ export default function PayForGroupModal({ isOpen, onClose, group, payerLogin, b
)}
</td>
<td className="text-end">
{(d.member.amount ?? 0) > 0 ? `${d.member.amount} Kč` : <span className="text-muted">—</span>}
{(d.member.amount ?? 0) > 0 ? `${d.member.amount! / 100} Kč` : <span className="text-muted">—</span>}
</td>
<td className="text-end">
{surcharge > 0 ? `${surcharge} Kč` : <span className="text-muted">—</span>}
{surcharge > 0 ? `${surcharge / 100} Kč` : <span className="text-muted">—</span>}
</td>
{hasFees && (
<td className="text-end">
{feeShare > 0 ? `${feeShare} Kč` : '—'}
{feeShare > 0 ? `${feeShare / 100} Kč` : '—'}
</td>
)}
<td className="text-end fw-bold">
{total > 0 ? `${total} Kč` : <span className="text-muted">—</span>}
{total > 0 ? `${total / 100} Kč` : <span className="text-muted">—</span>}
</td>
</tr>
);
@@ -15,12 +15,12 @@ export default function PizzaAdditionalFeeModal({ customerName, isOpen, onClose,
const priceRef = useRef<HTMLInputElement>(null);
const doSubmit = () => {
onSave(customerName, textRef.current?.value, Number.parseInt(priceRef.current?.value ?? "0"));
onSave(customerName, textRef.current?.value, Math.round(Number.parseFloat(priceRef.current?.value ?? "0") * 100));
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
onSave(customerName, textRef.current?.value, Number.parseInt(priceRef.current?.value ?? "0"));
onSave(customerName, textRef.current?.value, Math.round(Number.parseFloat(priceRef.current?.value ?? "0") * 100));
}
}
+16 -16
View File
@@ -125,7 +125,7 @@ export default function OrderGroupsPage() {
setPageError('Zadejte platnou kladnou částku');
return;
}
const ok = await refresh(() => updateGroupMember({ body: { id: groupId, login, amount: n } }));
const ok = await refresh(() => updateGroupMember({ body: { id: groupId, login, amount: Math.round(n * 100) } }));
if (ok) setEditAmounts(prev => { const next = { ...prev }; delete next[key]; return next; });
};
@@ -145,7 +145,7 @@ export default function OrderGroupsPage() {
setPageError('Zadejte platnou výši příplatku');
return;
}
const ok = await refresh(() => updateGroupMember({ body: { id: groupId, login, surchargeText, surchargeAmount: rawAmount === '' ? 0 : surchargeAmount } }));
const ok = await refresh(() => updateGroupMember({ body: { id: groupId, login, surchargeText, surchargeAmount: rawAmount === '' ? 0 : Math.round(surchargeAmount * 100) } }));
if (ok) setEditSurcharges(prev => { const next = { ...prev }; delete next[key]; return next; });
};
@@ -254,17 +254,17 @@ export default function OrderGroupsPage() {
const editingTimes = group.id in editTimes;
const totalFees = (group.fees ?? 0) + (group.shipping ?? 0) + (group.tip ?? 0);
const feeShare = memberCount > 0 ? Math.round(totalFees / memberCount * 100) / 100 : 0;
const feeShare = memberCount > 0 ? Math.round(totalFees / memberCount) : 0;
const getMemberTotal = (m: OrderGroupMember) => {
const base = m.amount ?? 0;
const surcharge = m.surchargeAmount ?? 0;
const dv = group.discountValue ?? 0;
const discount = dv > 0
? (group.discountType === 'percent'
? Math.round((base + surcharge) * dv / 100 * 100) / 100
: Math.round(dv / memberCount * 100) / 100)
? Math.round((base + surcharge) * dv / 100)
: Math.round(dv / memberCount))
: 0;
return Math.round((base + surcharge + feeShare - discount) * 100) / 100;
return base + surcharge + feeShare - discount;
};
return (
@@ -370,10 +370,10 @@ export default function OrderGroupsPage() {
) : (
<span
style={{ cursor: canEdit ? 'pointer' : undefined }}
onClick={() => canEdit && setEditAmounts(prev => ({ ...prev, [key]: String(member.amount ?? '') }))}
onClick={() => canEdit && setEditAmounts(prev => ({ ...prev, [key]: member.amount != null ? String(member.amount / 100) : '' }))}
title={canEdit ? 'Klikněte pro úpravu' : undefined}
>
{member.amount != null ? `${member.amount}` : <span className="text-muted"></span>}
{member.amount != null ? `${member.amount / 100}` : <span className="text-muted"></span>}
</span>
)}
</td>
@@ -404,11 +404,11 @@ export default function OrderGroupsPage() {
) : (
<span
style={{ cursor: canEdit ? 'pointer' : undefined }}
onClick={() => canEdit && setEditSurcharges(prev => ({ ...prev, [key]: { text: member.surchargeText ?? '', amount: member.surchargeAmount != null ? String(member.surchargeAmount) : '' } }))}
onClick={() => canEdit && setEditSurcharges(prev => ({ ...prev, [key]: { text: member.surchargeText ?? '', amount: member.surchargeAmount != null ? String(member.surchargeAmount / 100) : '' } }))}
title={canEdit ? 'Klikněte pro úpravu příplatku' : undefined}
>
{member.surchargeAmount != null && member.surchargeAmount > 0 ? (
<small>{member.surchargeText ? `${member.surchargeText}: ` : ''}<strong>{member.surchargeAmount} </strong></small>
<small>{member.surchargeText ? `${member.surchargeText}: ` : ''}<strong>{member.surchargeAmount / 100} </strong></small>
) : (
<small className="text-muted"></small>
)}
@@ -440,7 +440,7 @@ export default function OrderGroupsPage() {
</td>
<td className="text-end">
<small className={memberTotal > 0 ? 'fw-bold' : 'text-muted'}>
{memberTotal > 0 ? `${memberTotal}` : '—'}
{memberTotal > 0 ? `${memberTotal / 100}` : '—'}
</small>
</td>
<td>
@@ -464,13 +464,13 @@ export default function OrderGroupsPage() {
{/* Souhrn poplatků a slevy */}
{(totalFees > 0 || (group.discountValue != null && group.discountValue > 0)) && (
<div className="px-3 py-2 border-top d-flex gap-3 flex-wrap" style={{ fontSize: '0.85em', color: 'var(--luncher-text-muted)' }}>
{group.fees != null && group.fees > 0 && <span>Poplatky: <strong>{group.fees} </strong></span>}
{group.shipping != null && group.shipping > 0 && <span>Doprava: <strong>{group.shipping} </strong></span>}
{group.tip != null && group.tip > 0 && <span>Spropitné: <strong>{group.tip} </strong></span>}
{feeShare > 0 && <span> <strong>{feeShare} </strong>/os.</span>}
{group.fees != null && group.fees > 0 && <span>Poplatky: <strong>{group.fees / 100} </strong></span>}
{group.shipping != null && group.shipping > 0 && <span>Doprava: <strong>{group.shipping / 100} </strong></span>}
{group.tip != null && group.tip > 0 && <span>Spropitné: <strong>{group.tip / 100} </strong></span>}
{feeShare > 0 && <span> <strong>{feeShare / 100} </strong>/os.</span>}
{group.discountValue != null && group.discountValue > 0 && (
<span className="text-success">
Sleva: <strong>{group.discountType === 'percent' ? `${group.discountValue}%` : `${group.discountValue}`}</strong>
Sleva: <strong>{group.discountType === 'percent' ? `${group.discountValue}%` : `${group.discountValue / 100}`}</strong>
</span>
)}
</div>
+9 -9
View File
@@ -28,16 +28,16 @@ const buildPizzaUrl = (pizzaUrl: string) => {
return `${baseUrl}/${pizzaUrl}`;
}
// Ceny krabic dle velikosti
// Ceny krabic dle velikosti v haléřích
const boxPrices: { [key: string]: number } = {
"30cm": 13,
"35cm": 15,
"40cm": 18,
"50cm": 25
"30cm": 1300,
"35cm": 1500,
"40cm": 1800,
"50cm": 2500
}
// Cena obalu pro salát
const SALAT_BOX_PRICE = 13;
// Cena obalu pro salát v haléřích
const SALAT_BOX_PRICE = 1300;
/**
* Stáhne a scrapne aktuální pizzy ze stránek Pizza Chefie.
@@ -79,7 +79,7 @@ export async function downloadPizzy(mock: boolean): Promise<Pizza[]> {
a.each((i, elm) => {
const varId = Number.parseInt(elm.attribs.href.split('?varianta=')[1].trim());
const size = $($(elm).contents().get(0)).text().trim();
const price = Number.parseInt($($(elm).contents().get(1)).text().trim().split(" Kč")[0]);
const price = Number.parseInt($($(elm).contents().get(1)).text().trim().split(" Kč")[0]) * 100;
sizes.push({ varId, size, pizzaPrice: price, boxPrice: boxPrices[size], price: price + boxPrices[size] });
})
result.push({
@@ -119,7 +119,7 @@ export async function downloadSalaty(mock: boolean): Promise<Salat[]> {
ingredients.push($(elm).text());
});
const priceText = $('.cena > span', salatHtml).first().text().trim();
const price = Number.parseInt(priceText.split(' Kč')[0]);
const price = Number.parseInt(priceText.split(' Kč')[0]) * 100;
result.push({ name, ingredients, price: price + SALAT_BOX_PRICE });
}
return result;
+220 -220
View File
@@ -661,30 +661,30 @@ const MOCK_PIZZA_LIST = [
{
varId: 1,
size: "30cm",
pizzaPrice: 138,
boxPrice: 13,
price: 151
pizzaPrice: 13800,
boxPrice: 1300,
price: 15100
},
{
varId: 2,
size: "35cm",
pizzaPrice: 166,
boxPrice: 15,
price: 181
pizzaPrice: 16600,
boxPrice: 1500,
price: 18100
},
{
varId: 3,
size: "40cm",
pizzaPrice: 223,
boxPrice: 18,
price: 241
pizzaPrice: 22300,
boxPrice: 1800,
price: 24100
},
{
varId: 4,
size: "50cm",
pizzaPrice: 306,
boxPrice: 25,
price: 331
pizzaPrice: 30600,
boxPrice: 2500,
price: 33100
}
]
},
@@ -700,30 +700,30 @@ const MOCK_PIZZA_LIST = [
{
varId: 6,
size: "30cm",
pizzaPrice: 142,
boxPrice: 13,
price: 155
pizzaPrice: 14200,
boxPrice: 1300,
price: 15500
},
{
varId: 7,
size: "35cm",
pizzaPrice: 172,
boxPrice: 15,
price: 187
pizzaPrice: 17200,
boxPrice: 1500,
price: 18700
},
{
varId: 8,
size: "40cm",
pizzaPrice: 233,
boxPrice: 18,
price: 251
pizzaPrice: 23300,
boxPrice: 1800,
price: 25100
},
{
varId: 9,
size: "50cm",
pizzaPrice: 316,
boxPrice: 25,
price: 341
pizzaPrice: 31600,
boxPrice: 2500,
price: 34100
}
]
},
@@ -741,30 +741,30 @@ const MOCK_PIZZA_LIST = [
{
varId: 10,
size: "30cm",
pizzaPrice: 142,
boxPrice: 13,
price: 155
pizzaPrice: 14200,
boxPrice: 1300,
price: 15500
},
{
varId: 11,
size: "35cm",
pizzaPrice: 172,
boxPrice: 15,
price: 187
pizzaPrice: 17200,
boxPrice: 1500,
price: 18700
},
{
varId: 12,
size: "40cm",
pizzaPrice: 233,
boxPrice: 18,
price: 251
pizzaPrice: 23300,
boxPrice: 1800,
price: 25100
},
{
varId: 13,
size: "50cm",
pizzaPrice: 316,
boxPrice: 25,
price: 341
pizzaPrice: 31600,
boxPrice: 2500,
price: 34100
}
]
},
@@ -780,30 +780,30 @@ const MOCK_PIZZA_LIST = [
{
varId: 14,
size: "30cm",
pizzaPrice: 142,
boxPrice: 13,
price: 155
pizzaPrice: 14200,
boxPrice: 1300,
price: 15500
},
{
varId: 15,
size: "35cm",
pizzaPrice: 172,
boxPrice: 15,
price: 187
pizzaPrice: 17200,
boxPrice: 1500,
price: 18700
},
{
varId: 16,
size: "40cm",
pizzaPrice: 233,
boxPrice: 18,
price: 251
pizzaPrice: 23300,
boxPrice: 1800,
price: 25100
},
{
varId: 17,
size: "50cm",
pizzaPrice: 294,
boxPrice: 25,
price: 319
pizzaPrice: 29400,
boxPrice: 2500,
price: 31900
}
]
},
@@ -821,30 +821,30 @@ const MOCK_PIZZA_LIST = [
{
varId: 22,
size: "30cm",
pizzaPrice: 162,
boxPrice: 13,
price: 175
pizzaPrice: 16200,
boxPrice: 1300,
price: 17500
},
{
varId: 23,
size: "35cm",
pizzaPrice: 186,
boxPrice: 15,
price: 201
pizzaPrice: 18600,
boxPrice: 1500,
price: 20100
},
{
varId: 24,
size: "40cm",
pizzaPrice: 263,
boxPrice: 18,
price: 281
pizzaPrice: 26300,
boxPrice: 1800,
price: 28100
},
{
varId: 25,
size: "50cm",
pizzaPrice: 346,
boxPrice: 25,
price: 371
pizzaPrice: 34600,
boxPrice: 2500,
price: 37100
}
]
},
@@ -861,30 +861,30 @@ const MOCK_PIZZA_LIST = [
{
varId: 26,
size: "30cm",
pizzaPrice: 162,
boxPrice: 13,
price: 175
pizzaPrice: 16200,
boxPrice: 1300,
price: 17500
},
{
varId: 27,
size: "35cm",
pizzaPrice: 186,
boxPrice: 15,
price: 201
pizzaPrice: 18600,
boxPrice: 1500,
price: 20100
},
{
varId: 28,
size: "40cm",
pizzaPrice: 263,
boxPrice: 18,
price: 281
pizzaPrice: 26300,
boxPrice: 1800,
price: 28100
},
{
varId: 29,
size: "50cm",
pizzaPrice: 346,
boxPrice: 25,
price: 371
pizzaPrice: 34600,
boxPrice: 2500,
price: 37100
}
]
},
@@ -902,30 +902,30 @@ const MOCK_PIZZA_LIST = [
{
varId: 30,
size: "30cm",
pizzaPrice: 162,
boxPrice: 13,
price: 175
pizzaPrice: 16200,
boxPrice: 1300,
price: 17500
},
{
varId: 31,
size: "35cm",
pizzaPrice: 186,
boxPrice: 15,
price: 201
pizzaPrice: 18600,
boxPrice: 1500,
price: 20100
},
{
varId: 32,
size: "40cm",
pizzaPrice: 263,
boxPrice: 18,
price: 281
pizzaPrice: 26300,
boxPrice: 1800,
price: 28100
},
{
varId: 33,
size: "50cm",
pizzaPrice: 346,
boxPrice: 25,
price: 371
pizzaPrice: 34600,
boxPrice: 2500,
price: 37100
}
]
},
@@ -946,30 +946,30 @@ const MOCK_PIZZA_LIST = [
{
varId: 34,
size: "30cm",
pizzaPrice: 162,
boxPrice: 13,
price: 175
pizzaPrice: 16200,
boxPrice: 1300,
price: 17500
},
{
varId: 35,
size: "35cm",
pizzaPrice: 186,
boxPrice: 15,
price: 201
pizzaPrice: 18600,
boxPrice: 1500,
price: 20100
},
{
varId: 36,
size: "40cm",
pizzaPrice: 263,
boxPrice: 18,
price: 281
pizzaPrice: 26300,
boxPrice: 1800,
price: 28100
},
{
varId: 37,
size: "50cm",
pizzaPrice: 346,
boxPrice: 25,
price: 371
pizzaPrice: 34600,
boxPrice: 2500,
price: 37100
}
]
},
@@ -987,30 +987,30 @@ const MOCK_PIZZA_LIST = [
{
varId: 38,
size: "30cm",
pizzaPrice: 162,
boxPrice: 13,
price: 175
pizzaPrice: 16200,
boxPrice: 1300,
price: 17500
},
{
varId: 39,
size: "35cm",
pizzaPrice: 186,
boxPrice: 15,
price: 201
pizzaPrice: 18600,
boxPrice: 1500,
price: 20100
},
{
varId: 40,
size: "40cm",
pizzaPrice: 263,
boxPrice: 18,
price: 281
pizzaPrice: 26300,
boxPrice: 1800,
price: 28100
},
{
varId: 41,
size: "50cm",
pizzaPrice: 346,
boxPrice: 25,
price: 371
pizzaPrice: 34600,
boxPrice: 2500,
price: 37100
}
]
},
@@ -1028,30 +1028,30 @@ const MOCK_PIZZA_LIST = [
{
varId: 42,
size: "30cm",
pizzaPrice: 172,
boxPrice: 13,
price: 185
pizzaPrice: 17200,
boxPrice: 1300,
price: 18500
},
{
varId: 43,
size: "35cm",
pizzaPrice: 212,
boxPrice: 15,
price: 227
pizzaPrice: 21200,
boxPrice: 1500,
price: 22700
},
{
varId: 44,
size: "40cm",
pizzaPrice: 293,
boxPrice: 18,
price: 311
pizzaPrice: 29300,
boxPrice: 1800,
price: 31100
},
{
varId: 45,
size: "50cm",
pizzaPrice: 376,
boxPrice: 25,
price: 401
pizzaPrice: 37600,
boxPrice: 2500,
price: 40100
}
]
},
@@ -1069,30 +1069,30 @@ const MOCK_PIZZA_LIST = [
{
varId: 46,
size: "30cm",
pizzaPrice: 182,
boxPrice: 13,
price: 195
pizzaPrice: 18200,
boxPrice: 1300,
price: 19500
},
{
varId: 47,
size: "35cm",
pizzaPrice: 222,
boxPrice: 15,
price: 237
pizzaPrice: 22200,
boxPrice: 1500,
price: 23700
},
{
varId: 48,
size: "40cm",
pizzaPrice: 303,
boxPrice: 18,
price: 321
pizzaPrice: 30300,
boxPrice: 1800,
price: 32100
},
{
varId: 49,
size: "50cm",
pizzaPrice: 386,
boxPrice: 25,
price: 411
pizzaPrice: 38600,
boxPrice: 2500,
price: 41100
}
]
},
@@ -1114,30 +1114,30 @@ const MOCK_PIZZA_LIST = [
{
varId: 50,
size: "30cm",
pizzaPrice: 182,
boxPrice: 13,
price: 195
pizzaPrice: 18200,
boxPrice: 1300,
price: 19500
},
{
varId: 51,
size: "35cm",
pizzaPrice: 222,
boxPrice: 15,
price: 237
pizzaPrice: 22200,
boxPrice: 1500,
price: 23700
},
{
varId: 52,
size: "40cm",
pizzaPrice: 303,
boxPrice: 18,
price: 321
pizzaPrice: 30300,
boxPrice: 1800,
price: 32100
},
{
varId: 53,
size: "50cm",
pizzaPrice: 396,
boxPrice: 25,
price: 421
pizzaPrice: 39600,
boxPrice: 2500,
price: 42100
}
]
},
@@ -1156,30 +1156,30 @@ const MOCK_PIZZA_LIST = [
{
varId: 54,
size: "30cm",
pizzaPrice: 182,
boxPrice: 13,
price: 195
pizzaPrice: 18200,
boxPrice: 1300,
price: 19500
},
{
varId: 55,
size: "35cm",
pizzaPrice: 222,
boxPrice: 15,
price: 237
pizzaPrice: 22200,
boxPrice: 1500,
price: 23700
},
{
varId: 56,
size: "40cm",
pizzaPrice: 303,
boxPrice: 18,
price: 321
pizzaPrice: 30300,
boxPrice: 1800,
price: 32100
},
{
varId: 57,
size: "50cm",
pizzaPrice: 396,
boxPrice: 25,
price: 421
pizzaPrice: 39600,
boxPrice: 2500,
price: 42100
}
]
},
@@ -1199,30 +1199,30 @@ const MOCK_PIZZA_LIST = [
{
varId: 58,
size: "30cm",
pizzaPrice: 182,
boxPrice: 13,
price: 195
pizzaPrice: 18200,
boxPrice: 1300,
price: 19500
},
{
varId: 59,
size: "35cm",
pizzaPrice: 222,
boxPrice: 15,
price: 237
pizzaPrice: 22200,
boxPrice: 1500,
price: 23700
},
{
varId: 60,
size: "40cm",
pizzaPrice: 303,
boxPrice: 18,
price: 321
pizzaPrice: 30300,
boxPrice: 1800,
price: 32100
},
{
varId: 61,
size: "50cm",
pizzaPrice: 396,
boxPrice: 25,
price: 421
pizzaPrice: 39600,
boxPrice: 2500,
price: 42100
}
]
},
@@ -1241,30 +1241,30 @@ const MOCK_PIZZA_LIST = [
{
varId: 62,
size: "30cm",
pizzaPrice: 188,
boxPrice: 13,
price: 201
pizzaPrice: 18800,
boxPrice: 1300,
price: 20100
},
{
varId: 63,
size: "35cm",
pizzaPrice: 226,
boxPrice: 15,
price: 241
pizzaPrice: 22600,
boxPrice: 1500,
price: 24100
},
{
varId: 64,
size: "40cm",
pizzaPrice: 313,
boxPrice: 18,
price: 331
pizzaPrice: 31300,
boxPrice: 1800,
price: 33100
},
{
varId: 65,
size: "50cm",
pizzaPrice: 426,
boxPrice: 25,
price: 451
pizzaPrice: 42600,
boxPrice: 2500,
price: 45100
}
]
},
@@ -1283,30 +1283,30 @@ const MOCK_PIZZA_LIST = [
{
varId: 66,
size: "30cm",
pizzaPrice: 188,
boxPrice: 13,
price: 201
pizzaPrice: 18800,
boxPrice: 1300,
price: 20100
},
{
varId: 67,
size: "35cm",
pizzaPrice: 226,
boxPrice: 15,
price: 241
pizzaPrice: 22600,
boxPrice: 1500,
price: 24100
},
{
varId: 68,
size: "40cm",
pizzaPrice: 313,
boxPrice: 18,
price: 331
pizzaPrice: 31300,
boxPrice: 1800,
price: 33100
},
{
varId: 69,
size: "50cm",
pizzaPrice: 426,
boxPrice: 25,
price: 451
pizzaPrice: 42600,
boxPrice: 2500,
price: 45100
}
]
},
@@ -1327,30 +1327,30 @@ const MOCK_PIZZA_LIST = [
{
varId: 309,
size: "30cm",
pizzaPrice: 182,
boxPrice: 13,
price: 195
pizzaPrice: 18200,
boxPrice: 1300,
price: 19500
},
{
varId: 310,
size: "35cm",
pizzaPrice: 222,
boxPrice: 15,
price: 237
pizzaPrice: 22200,
boxPrice: 1500,
price: 23700
},
{
varId: 311,
size: "40cm",
pizzaPrice: 303,
boxPrice: 18,
price: 321
pizzaPrice: 30300,
boxPrice: 1800,
price: 32100
},
{
varId: 312,
size: "50cm",
pizzaPrice: 396,
boxPrice: 25,
price: 421
pizzaPrice: 39600,
boxPrice: 2500,
price: 42100
}
]
},
@@ -1369,30 +1369,30 @@ const MOCK_PIZZA_LIST = [
{
varId: 394,
size: "30cm",
pizzaPrice: 188,
boxPrice: 13,
price: 201
pizzaPrice: 18800,
boxPrice: 1300,
price: 20100
},
{
varId: 395,
size: "35cm",
pizzaPrice: 226,
boxPrice: 15,
price: 241
pizzaPrice: 22600,
boxPrice: 1500,
price: 24100
},
{
varId: 396,
size: "40cm",
pizzaPrice: 313,
boxPrice: 18,
price: 331
pizzaPrice: 31300,
boxPrice: 1800,
price: 33100
},
{
varId: 397,
size: "50cm",
pizzaPrice: 426,
boxPrice: 25,
price: 451
pizzaPrice: 42600,
boxPrice: 2500,
price: 45100
}
]
}
@@ -1434,22 +1434,22 @@ const MOCK_SALAT_LIST = [
{
name: "Greek",
ingredients: ["Salát", "Černé olivy", "Paprika mix", "Červená cibule", "Rajčata", "Okurka salátová", "Jogurtový dresing"],
price: 174 + 13,
price: (174 + 13) * 100,
},
{
name: "Caesar",
ingredients: ["Salát", "Rajčata", "Kuřecí maso", "Krutony", "Parmazán", "Caesar dresing", "Olivový olej"],
price: 184 + 13,
price: (184 + 13) * 100,
},
{
name: "Šopský salát",
ingredients: ["Salátová okurka", "Rajčata", "Paprika mix", "Červená cibule", "Balkánský sýr"],
price: 164 + 13,
price: (164 + 13) * 100,
},
{
name: "Těstovinový salát",
ingredients: ["Penne", "Okurka", "Rajčata", "Paprika mix", "Kuřecí maso", "Jogurtový dresing"],
price: 184 + 13,
price: (184 + 13) * 100,
},
]
+1 -1
View File
@@ -342,7 +342,7 @@ export async function finishPizzaDelivery(login: string, bankAccount?: string, b
let message = order.pizzaList!.map(item =>
item.category === 'salat' ? `Salát ${item.name}` : `Pizza ${item.name} (${item.size})`
).join(', ');
await generateQr(order.customer, bankAccount, bankAccountHolder, order.totalPrice, message, id);
await generateQr(order.customer, bankAccount, bankAccountHolder, order.totalPrice / 100, message, id);
order.hasQr = true;
// Uložíme nevyřízený QR kód pro persistentní zobrazení
await addPendingQr(order.customer, {
+6 -6
View File
@@ -69,7 +69,7 @@ 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 (typeof amount !== 'number' || !Number.isFinite(amount) || amount < 0) {
if (!Number.isInteger(amount) || amount < 0) {
return res.status(400).json({ error: 'Neplatná částka' });
}
patch.amount = amount;
@@ -83,7 +83,7 @@ router.post("/updateMember", async (req: Request, res, next) => {
patch.surchargeText = surchargeText;
}
if (surchargeAmount !== undefined) {
if (typeof surchargeAmount !== 'number' || !Number.isFinite(surchargeAmount) || surchargeAmount < 0) {
if (!Number.isInteger(surchargeAmount) || surchargeAmount < 0) {
return res.status(400).json({ error: 'Neplatná výše příplatku' });
}
patch.surchargeAmount = surchargeAmount;
@@ -113,19 +113,19 @@ 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 && (typeof fees !== 'number' || !Number.isFinite(fees) || fees < 0)) {
if (fees !== undefined && (!Number.isInteger(fees) || fees < 0)) {
return res.status(400).json({ error: 'Neplatná výše poplatků' });
}
if (shipping !== undefined && (typeof shipping !== 'number' || !Number.isFinite(shipping) || shipping < 0)) {
if (shipping !== undefined && (!Number.isInteger(shipping) || shipping < 0)) {
return res.status(400).json({ error: 'Neplatná výše dopravy' });
}
if (tip !== undefined && (typeof tip !== 'number' || !Number.isFinite(tip) || tip < 0)) {
if (tip !== undefined && (!Number.isInteger(tip) || tip < 0)) {
return res.status(400).json({ error: 'Neplatná výše spropitného' });
}
if (discountType !== undefined && discountType !== '' && !['percent', 'fixed'].includes(discountType)) {
return res.status(400).json({ error: 'Neplatný typ slevy' });
}
if (discountValue !== undefined && (typeof discountValue !== 'number' || !Number.isFinite(discountValue) || discountValue < 0)) {
if (discountValue !== undefined && (!Number.isInteger(discountValue) || discountValue < 0)) {
return res.status(400).json({ error: 'Neplatná výše slevy' });
}
try {
+2 -7
View File
@@ -36,18 +36,13 @@ router.post("/generate", async (req: Request<{}, any, GenerateQrData["body"]>, r
if (!recipient.purpose || recipient.purpose.trim().length === 0) {
return res.status(400).json({ error: `Příjemce ${recipient.login} nemá vyplněný účel platby` });
}
if (typeof recipient.amount !== 'number' || recipient.amount <= 0) {
if (!Number.isInteger(recipient.amount) || recipient.amount <= 0) {
return res.status(400).json({ error: `Příjemce ${recipient.login} má neplatnou částku` });
}
// Validace max 2 desetinná místa
const amountStr = recipient.amount.toString();
if (amountStr.includes('.') && amountStr.split('.')[1].length > 2) {
return res.status(400).json({ error: `Částka pro ${recipient.login} má více než 2 desetinná místa` });
}
// Vygenerovat QR kód
const id = crypto.randomUUID();
await generateQr(recipient.login, bankAccount, bankAccountHolder, recipient.amount, recipient.purpose, id);
await generateQr(recipient.login, bankAccount, bankAccountHolder, recipient.amount / 100, recipient.purpose, id);
// Uložit jako nevyřízený QR kód a okamžitě doručit příjemci
const pendingQr = {
+4 -4
View File
@@ -31,8 +31,8 @@ test('saláty mají name a ingredients', async () => {
test('cena salátu zahrnuje pevný příplatek 13 Kč za obal', async () => {
const salaty = await downloadSalaty(false);
// Caesar sticker price = 129, box = 13
expect(salaty[0].price).toBe(129 + 13);
// Řecký sticker price = 119, box = 13
expect(salaty[1].price).toBe(119 + 13);
// Caesar sticker price = 129, box = 13 → (129 + 13) * 100 haléřů
expect(salaty[0].price).toBe((129 + 13) * 100);
// Řecký sticker price = 119, box = 13 → (119 + 13) * 100 haléřů
expect(salaty[1].price).toBe((119 + 13) * 100);
});
+5 -5
View File
@@ -28,8 +28,8 @@ beforeEach(() => {
const VALID_BODY = {
recipients: [
{ login: 'uzivatel1', purpose: 'Pizza Margherita', amount: 149 },
{ login: 'uzivatel2', purpose: 'Pizza Diavola', amount: 179 },
{ login: 'uzivatel1', purpose: 'Pizza Margherita', amount: 14900 },
{ login: 'uzivatel2', purpose: 'Pizza Diavola', amount: 17900 },
],
bankAccount: '19-2000145399/0800',
bankAccountHolder: 'Jan Novák',
@@ -76,17 +76,17 @@ test('POST /generate vrátí 400 pro zápornou částku', async () => {
expect(res.body.error).toContain('částku');
});
test('POST /generate vrátí 400 pro částku s více než 2 desetinnými místy', async () => {
test('POST /generate vrátí 400 pro necelou částku', async () => {
const body = {
...VALID_BODY,
recipients: [{ login: 'uzivatel1', purpose: 'Pizza', amount: 149.999 }],
recipients: [{ login: 'uzivatel1', purpose: 'Pizza', amount: 149.5 }],
};
const res = await request(buildApp())
.post('/api/qr/generate')
.set('Authorization', TOKEN)
.send(body);
expect(res.status).toBe(400);
expect(res.body.error).toContain('desetinná');
expect(res.body.error).toContain('částku');
});
test('POST /generate vrátí 400 pro příjemce bez login', async () => {
+8 -8
View File
@@ -14,21 +14,21 @@ post:
description: ID skupiny
type: string
fees:
description: Poplatky ()
type: number
description: Poplatky (haléře)
type: integer
shipping:
description: Doprava ()
type: number
description: Doprava (haléře)
type: integer
tip:
description: Spropitné ()
type: number
description: Spropitné (haléře)
type: integer
discountType:
description: Typ slevy
type: string
enum: [percent, fixed]
discountValue:
description: Hodnota slevy
type: number
description: Hodnota slevy (procenta nebo haléře pro fixed)
type: integer
responses:
"200":
$ref: "../../api.yml#/components/responses/ClientDataResponse"
+4 -4
View File
@@ -18,8 +18,8 @@ post:
description: Login člena ke změně
type: string
amount:
description: Částka k úhradě v
type: number
description: Částka k úhradě v haléřích
type: integer
note:
description: Poznámka
type: string
@@ -27,8 +27,8 @@ post:
description: Popis příplatku
type: string
surchargeAmount:
description: Výše příplatku v
type: number
description: Výše příplatku v haléřích
type: integer
responses:
"200":
$ref: "../../api.yml#/components/responses/ClientDataResponse"
+2 -2
View File
@@ -16,8 +16,8 @@ post:
type: string
description: Textový popis přirážky/slevy
price:
type: number
description: Částka přirážky/slevy v
type: integer
description: Částka přirážky/slevy v haléřích
responses:
"200":
description: Nastavení přirážky/slevy proběhlo úspěšně.
+31 -31
View File
@@ -420,14 +420,14 @@ PizzaSize:
description: Velikost pizzy, např. "30cm"
type: string
pizzaPrice:
description: Cena samotné pizzy v
type: number
description: Cena samotné pizzy v haléřích
type: integer
boxPrice:
description: Cena krabice pizzy v
type: number
description: Cena krabice pizzy v haléřích
type: integer
price:
description: Celková cena (pizza + krabice)
type: number
description: Celková cena (pizza + krabice) v haléřích
type: integer
Pizza:
description: Údaje o konkrétní pizze.
type: object
@@ -470,8 +470,8 @@ PizzaVariant:
description: Velikost pizzy (např. "30cm"), nebo "1 porce" pro salát
type: string
price:
description: Cena v , včetně krabice/obalu
type: number
description: Cena v haléřích, včetně krabice/obalu
type: integer
category:
description: Kategorie položky (pizza nebo salat)
type: string
@@ -494,8 +494,8 @@ Salat:
items:
type: string
price:
description: Cena salátu v (bez obalu)
type: number
description: Cena salátu v haléřích (bez obalu)
type: integer
PizzaOrder:
description: Údaje o objednávce pizzy jednoho uživatele.
type: object
@@ -521,11 +521,11 @@ PizzaOrder:
description: Popis příplatku (např. "kuřecí maso navíc")
type: string
price:
description: Cena příplatku v
type: number
description: Cena příplatku v haléřích
type: integer
totalPrice:
description: Celková cena všech objednaných pizz daného uživatele, včetně krabic a příplatků
type: number
description: Celková cena všech objednaných pizz daného uživatele v haléřích, včetně krabic a příplatků
type: integer
hasQr:
description: |
Příznak, pokud je k této objednávce vygenerován QR kód pro platbu. To je typicky pravda, pokud:
@@ -635,9 +635,9 @@ QrRecipient:
description: Účel platby (např. "Pizza prosciutto")
type: string
amount:
description: Částka v (kladné číslo, max 2 desetinná místa)
type: number
minimum: 0.01
description: Částka v haléřích (kladné celé číslo)
type: integer
minimum: 1
GenerateQrRequest:
description: Request pro generování QR kódů
type: object
@@ -704,8 +704,8 @@ OrderGroupMember:
additionalProperties: false
properties:
amount:
description: Částka k úhradě v
type: number
description: Částka k úhradě v haléřích
type: integer
note:
description: Volitelná poznámka (např. co si objednává)
type: string
@@ -713,8 +713,8 @@ OrderGroupMember:
description: Popis příplatku
type: string
surchargeAmount:
description: Výše příplatku v
type: number
description: Výše příplatku v haléřích
type: integer
paid:
description: Příznak, zda člen uhradil svůj podíl objednávky
type: boolean
@@ -753,21 +753,21 @@ OrderGroup:
description: Očekávaný čas doručení ve formátu HH:MM
type: string
fees:
description: Poplatky (balení apod.) celkem v
type: number
description: Poplatky (balení apod.) celkem v haléřích
type: integer
shipping:
description: Doprava v
type: number
description: Doprava v haléřích
type: integer
tip:
description: Spropitné v
type: number
description: Spropitné v haléřích
type: integer
discountType:
description: Typ slevy aplikované na objednávku
type: string
enum: [percent, fixed]
discountValue:
description: Hodnota slevy (procenta nebo Kč)
type: number
description: Hodnota slevy (procenta pro typ 'percent', haléře pro typ 'fixed')
type: integer
# --- NEVYŘÍZENÉ QR KÓDY ---
PendingQr:
@@ -790,8 +790,8 @@ PendingQr:
description: Jméno uživatele, který QR vygeneroval (příjemce platby)
type: string
totalPrice:
description: Celková cena objednávky v
type: number
description: Celková cena objednávky v haléřích
type: integer
purpose:
description: Účel platby (např. "Pizza prosciutto")
type: string