feat: zadávání cen
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 28s
CI / Build server (push) Successful in 37s
CI / Build client (push) Successful in 45s
CI / Playwright E2E tests (push) Successful in 2m15s
CI / Build and push Docker image (push) Successful in 45s
CI / Notify (push) Successful in 2s
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 28s
CI / Build server (push) Successful in 37s
CI / Build client (push) Successful in 45s
CI / Playwright E2E tests (push) Successful in 2m15s
CI / Build and push Docker image (push) Successful in 45s
CI / Notify (push) Successful in 2s
This commit is contained in:
1 parent
96dc3f0053
commit
9a3987a2fa
13 files changed
+244
-11
No files matched your search
@@ -0,0 +1,3 @@
|
||||
[
|
||||
"U oběda v restauraci si můžete zadat cenu (ikonka cenovky u vaší volby) — promítne se do měsíčního přehledu stravování a předvyplní se i v „Zaplatit za všechny“"
|
||||
]
|
||||
@@ -1,10 +1,10 @@
|
||||
import express, { Request, Response } from "express";
|
||||
import { getLogin, getTrusted } from "../auth";
|
||||
import { addChoice, getActiveRestaurants, getDateForWeekIndex, getToday, removeChoice, removeChoices, updateDepartureTime, updateNote, fetchRestaurantWeekMenuData, saveRestaurantWeekMenu, updateBuyer } from "../service";
|
||||
import { addChoice, getActiveRestaurants, getDateForWeekIndex, getToday, removeChoice, removeChoices, updateDepartureTime, updateNote, updatePrice, fetchRestaurantWeekMenuData, saveRestaurantWeekMenu, updateBuyer } from "../service";
|
||||
import { getDayOfWeekIndex, parseToken, getFirstWorkDayOfWeek } from "../utils";
|
||||
import { getWebsocket } from "../websocket";
|
||||
import { callNotifikace } from "../notifikace";
|
||||
import { AddChoiceData, ChangeDepartureTimeData, MealSlot, RemoveChoiceData, RemoveChoicesData, UdalostEnum, UpdateNoteData } from "../../../types/gen/types.gen";
|
||||
import { AddChoiceData, ChangeDepartureTimeData, MealSlot, RemoveChoiceData, RemoveChoicesData, UdalostEnum, UpdateNoteData, UpdatePriceData } from "../../../types/gen/types.gen";
|
||||
|
||||
|
||||
// RateLimit na refresh endpoint
|
||||
@@ -54,7 +54,7 @@ function checkRateLimit(key: string, limit: number = RATE_LIMIT): boolean {
|
||||
* @param req request
|
||||
* @returns index dne v týdnu
|
||||
*/
|
||||
const parseValidateFutureDayIndex = (req: Request<{}, any, AddChoiceData["body"] | UpdateNoteData["body"]>) => {
|
||||
const parseValidateFutureDayIndex = (req: Request<{}, any, AddChoiceData["body"] | UpdateNoteData["body"] | UpdatePriceData["body"]>) => {
|
||||
if (req.body.dayIndex == null) {
|
||||
throw new Error(`Nebyl předán index dne v týdnu.`);
|
||||
}
|
||||
@@ -171,6 +171,24 @@ router.post("/updateNote", async (req: Request<{}, any, UpdateNoteData["body"]>,
|
||||
} catch (e: any) { next(e) }
|
||||
});
|
||||
|
||||
router.post("/updatePrice", async (req: Request<{}, any, UpdatePriceData["body"]>, res, next) => {
|
||||
const login = getLogin(parseToken(req));
|
||||
const trusted = getTrusted(parseToken(req));
|
||||
let date = undefined;
|
||||
if (req.body.dayIndex != null) {
|
||||
try {
|
||||
date = getDateForWeekIndex(parseValidateFutureDayIndex(req));
|
||||
} catch (e: any) {
|
||||
return res.status(400).json({ error: e.message });
|
||||
}
|
||||
}
|
||||
try {
|
||||
const data = await updatePrice(login, trusted, req.body.price ?? undefined, date);
|
||||
getWebsocket().emit("message", data);
|
||||
res.status(200).json(data);
|
||||
} catch (e: any) { next(e) }
|
||||
});
|
||||
|
||||
router.post("/changeDepartureTime", async (req: Request<{}, any, ChangeDepartureTimeData["body"]>, res, next) => {
|
||||
const login = getLogin(parseToken(req));
|
||||
let date = undefined;
|
||||
|
||||
@@ -590,6 +590,37 @@ export async function updateNote(login: string, trusted: boolean, note?: string,
|
||||
});
|
||||
}
|
||||
|
||||
/** Volby, u kterých nemá smysl zadávat cenu ručně — cena je dána objednávkou nebo se neobědvá. */
|
||||
export const CHOICES_WITHOUT_PRICE: LunchChoice[] = [LunchChoice.OBJEDNAVAM, LunchChoice.PIZZA, LunchChoice.NEOBEDVAM, LunchChoice.ROZHODUJI];
|
||||
|
||||
/**
|
||||
* Nastaví nebo odstraní cenu oběda u aktuálně vybrané možnosti uživatele.
|
||||
*
|
||||
* @param login login uživatele
|
||||
* @param trusted příznak, zda se jedná o ověřeného uživatele
|
||||
* @param price cena v haléřích, při nevyplnění se cena odstraní
|
||||
* @param date datum, ke kterému se volba vztahuje
|
||||
*/
|
||||
export async function updatePrice(login: string, trusted: boolean, price?: number, date?: Date) {
|
||||
if (price != null && (!Number.isInteger(price) || price < 0)) {
|
||||
throw new Error(`Neplatná cena: ${price}`);
|
||||
}
|
||||
const usedDate = date ?? getToday();
|
||||
const snapshot = await getClientData(usedDate);
|
||||
validateTrusted(snapshot, login, trusted);
|
||||
return storage.updateData<ClientData>(getDataKey(usedDate), (current) => {
|
||||
const data = current ?? getEmptyData(date);
|
||||
const userEntry = Object.entries(data.choices ?? {}).find(entry => entry[1][login] != null);
|
||||
if (!userEntry) throw new Error("Cenu lze zadat jen k vybrané volbě");
|
||||
if (CHOICES_WITHOUT_PRICE.includes(userEntry[0] as LunchChoice)) {
|
||||
throw new Error("K této volbě nelze zadat cenu");
|
||||
}
|
||||
if (price == null) delete userEntry[1][login].price;
|
||||
else userEntry[1][login].price = price;
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualizuje preferovaný čas odchodu strávníka.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import getStorage from '../storage';
|
||||
import { resetMemoryStorage } from '../storage/memory';
|
||||
import { updatePrice } from '../service';
|
||||
import { ClientData } from '../../../types/gen/types.gen';
|
||||
|
||||
const storage = getStorage();
|
||||
const USER = 'petr';
|
||||
const DATE = new Date('2025-03-04');
|
||||
const ISO_DATE = '2025-03-04';
|
||||
|
||||
/** Uloží volby uživatelů pro testovací den. */
|
||||
async function saveChoices(choices: ClientData["choices"]) {
|
||||
await storage.setData<Partial<ClientData>>(ISO_DATE, { choices });
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
resetMemoryStorage();
|
||||
});
|
||||
|
||||
describe('updatePrice', () => {
|
||||
test('nastaví a odstraní cenu u volby podniku', async () => {
|
||||
await saveChoices({ SPSE: { [USER]: { note: 'polévka' } } });
|
||||
|
||||
let data = await updatePrice(USER, false, 9500, DATE);
|
||||
expect(data.choices.SPSE?.[USER]).toMatchObject({ price: 9500, note: 'polévka' });
|
||||
|
||||
data = await updatePrice(USER, false, undefined, DATE);
|
||||
expect(data.choices.SPSE?.[USER]?.price).toBeUndefined();
|
||||
expect(data.choices.SPSE?.[USER]?.note).toBe('polévka');
|
||||
});
|
||||
|
||||
test.each(['OBJEDNAVAM', 'PIZZA', 'NEOBEDVAM', 'ROZHODUJI'])('odmítne cenu u volby %s', async (choice) => {
|
||||
await saveChoices({ [choice]: { [USER]: {} } });
|
||||
await expect(updatePrice(USER, false, 10000, DATE)).rejects.toThrow('K této volbě nelze zadat cenu');
|
||||
});
|
||||
|
||||
test('odmítne cenu bez vybrané volby i zápornou cenu', async () => {
|
||||
await saveChoices({ SPSE: { tomas: {} } });
|
||||
await expect(updatePrice(USER, false, 10000, DATE)).rejects.toThrow('Cenu lze zadat jen k vybrané volbě');
|
||||
await expect(updatePrice(USER, false, -1, DATE)).rejects.toThrow('Neplatná cena');
|
||||
});
|
||||
});
|
||||
@@ -67,6 +67,14 @@ describe('getUserExportRows', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('u volby podniku vyplní uživatelem zadanou cenu', async () => {
|
||||
await saveLunch('2025-03-04', { SPSE: { [USER]: { price: 9500 } } });
|
||||
|
||||
const rows = await getUserExportRows(USER, YEAR, MONTH);
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]).toMatchObject({ type: 'SPŠE', amount: 9500 });
|
||||
});
|
||||
|
||||
test('u volby "budu objednávat" sloučí záznam s objednávkovou skupinou včetně ceny po slevě', async () => {
|
||||
await saveLunch('2025-03-06', { OBJEDNAVAM: { [USER]: { note: 'platím kartou' } } });
|
||||
await saveExtra('2025-03-06', [{
|
||||
|
||||
@@ -157,7 +157,7 @@ export async function getUserExportRows(login: string, year: number, month: numb
|
||||
|
||||
let food: string | undefined;
|
||||
let note = userChoice.note;
|
||||
let amount: number | undefined;
|
||||
let amount = userChoice.price;
|
||||
if (locationKey === LunchChoice.PIZZA) {
|
||||
const pizza = getPizzaText(lunchData, login);
|
||||
food = pizza.food;
|
||||
|
||||
Reference in new issue
Block a user