Files
Luncher/server/src/tests/userExport.test.ts
T
Ondřej Anděl 9a3987a2fa
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
feat: zadávání cen
2026-10-21 09:42:04 +02:00

332 lines
14 KiB
TypeScript

import getStorage from '../storage';
import { resetMemoryStorage } from '../storage/memory';
import { getUserExportRows, computeMemberAmount, getUserExportFileName, buildCsv, buildJson, generateUserExport, isExportFormat, UserExportRow } from '../userExport';
import { ClientData, GroupState, LunchChoice, OrderGroup, PizzaDayState, WeekMenu } from '../../../types/gen/types.gen';
const storage = getStorage();
const USER = 'petr';
const OTHER = 'tomas';
// 2025-03-03 je pondělí, 2025-03-04 úterý, ...
const YEAR = 2025;
const MONTH = 3;
/** Uloží denní data pro slot oběd. */
async function saveLunch(isoDate: string, choices: ClientData["choices"], pizzaDay?: ClientData["pizzaDay"]) {
await storage.setData<Partial<ClientData>>(isoDate, { choices, pizzaDay });
}
/** Uloží data slotu extra s předanými skupinami. */
async function saveExtra(isoDate: string, groups: OrderGroup[]) {
await storage.setData<Partial<ClientData>>(`${isoDate}_extra`, { choices: {}, groups });
}
/** Uloží týdenní menu tak, aby daný den obsahoval předaná jídla. */
async function saveWeekMenu(menuKey: string, dayIndex: number, foodNames: string[]) {
const week: WeekMenu = [{}, {}, {}, {}, {}] as WeekMenu;
week[dayIndex] = {
TECHTOWER: {
lastUpdate: 0,
closed: false,
food: foodNames.map(name => ({ name, isSoup: false })),
},
};
await storage.setData(menuKey, week);
}
beforeEach(() => {
resetMemoryStorage();
});
describe('getUserExportRows', () => {
test('vynechá dny bez záznamu i volbu "mám vlastní/neobědvám"', async () => {
await saveLunch('2025-03-03', { NEOBEDVAM: { [USER]: {} } });
await saveLunch('2025-03-04', { SPSE: { [USER]: {} } });
// den, kde má volbu jen jiný uživatel
await saveLunch('2025-03-05', { SPSE: { [OTHER]: {} } });
const rows = await getUserExportRows(USER, YEAR, MONTH);
expect(rows).toHaveLength(1);
expect(rows[0]).toMatchObject({ date: '2025-03-04', type: 'SPŠE' });
});
test('vyplní vybrané jídlo z uloženého menu i poznámku', async () => {
await saveWeekMenu('menu_2025_10', 0, ['Polévka', 'Svíčková', 'Guláš']);
await saveLunch('2025-03-03', {
TECHTOWER: { [USER]: { selectedFoods: [1, 2], note: 'bez knedlíku' } },
});
const rows = await getUserExportRows(USER, YEAR, MONTH);
expect(rows).toHaveLength(1);
expect(rows[0]).toMatchObject({
date: '2025-03-03',
dayOfWeek: 'pondělí',
type: 'TechTower',
food: 'Svíčková + Guláš',
note: 'bez knedlíku',
});
});
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', [{
id: 'g1',
name: 'KFC',
creatorLogin: OTHER,
state: GroupState.ORDERED,
orderedAt: '11:30',
shipping: 4000,
discountType: 'percent',
discountValue: 10,
members: {
[USER]: { amount: 20000, note: 'Twister menu', paid: true },
[OTHER]: { amount: 20000 },
},
}]);
const rows = await getUserExportRows(USER, YEAR, MONTH);
expect(rows).toHaveLength(1);
expect(rows[0]).toMatchObject({
date: '2025-03-06',
type: 'Budu objednávat',
food: 'Twister menu',
note: 'platím kartou',
store: 'KFC',
orderedBy: OTHER,
// 200 Kč + 20 Kč doprava (poloviny ze 40) - 20 Kč sleva (10 %)
amount: 20000 + 2000 - 2000,
});
});
test('objednávku bez volby oběda vypíše jako samostatný záznam', async () => {
await saveExtra('2025-03-07', [{
id: 'g2',
name: 'Bageterie',
creatorLogin: USER,
state: GroupState.OPEN,
members: { [USER]: { amount: 15000, surchargeText: 'sýr', surchargeAmount: 2000 } },
}]);
const rows = await getUserExportRows(USER, YEAR, MONTH);
expect(rows).toHaveLength(1);
expect(rows[0]).toMatchObject({
date: '2025-03-07',
type: 'Objednávka',
store: 'Bageterie',
orderedBy: USER,
food: 'příplatek: sýr',
amount: 17000,
});
});
test('u Pizza day vypíše objednané pizzy a celkovou cenu', async () => {
await saveLunch('2025-03-10', { PIZZA: { [USER]: {} } }, {
state: PizzaDayState.DELIVERED,
creator: OTHER,
orders: [{
customer: USER,
pizzaList: [{ varId: 1, name: 'Margherita', size: '32cm', price: 18000 }],
fee: { text: 'kuřecí maso', price: 3000 },
totalPrice: 21000,
note: 'bez oliv',
hasQr: true,
}],
} as ClientData["pizzaDay"]);
const rows = await getUserExportRows(USER, YEAR, MONTH);
expect(rows).toHaveLength(1);
expect(rows[0]).toMatchObject({
type: 'Pizza day',
food: 'Margherita (32cm) + příplatek: kuřecí maso',
note: 'bez oliv',
amount: 21000,
});
});
test('více skupin v jeden den vypíše jako samostatné řádky', async () => {
await saveLunch('2025-03-11', { OBJEDNAVAM: { [USER]: {} } });
await saveExtra('2025-03-11', [
{ id: 'a', name: 'KFC', creatorLogin: USER, state: GroupState.OPEN, members: { [USER]: { amount: 10000 } } },
{ id: 'b', name: 'Bageterie', creatorLogin: USER, state: GroupState.OPEN, members: { [USER]: { amount: 5000 } } },
]);
const rows = await getUserExportRows(USER, YEAR, MONTH);
expect(rows).toHaveLength(2);
// V rámci dne je nejdřív volba oběda (se sloučenou první skupinou), pak zbytek objednávek
expect(rows.map(r => r.type)).toEqual(['Budu objednávat', 'Objednávka']);
expect(rows.map(r => r.store)).toEqual(['KFC', 'Bageterie']);
});
test('nezahrne data mimo vybraný měsíc', async () => {
await saveLunch('2025-02-28', { SPSE: { [USER]: {} } });
await saveLunch('2025-03-03', { SPSE: { [USER]: {} } });
await saveLunch('2025-04-01', { SPSE: { [USER]: {} } });
const rows = await getUserExportRows(USER, YEAR, MONTH);
expect(rows.map(r => r.date)).toEqual(['2025-03-03']);
});
});
test('skupinu, kde si člen nic neobjednal a nemá poznámku, vynechá', async () => {
await saveLunch('2025-03-12', { OBJEDNAVAM: { [USER]: {} } });
await saveExtra('2025-03-12', [
{ id: 'g3', name: 'KFC', creatorLogin: USER, state: GroupState.ORDERED, members: { [USER]: {}, [OTHER]: { amount: 15000 } } },
{ id: 'g4', name: 'Namaste', creatorLogin: USER, state: GroupState.ORDERED, members: { [USER]: { amount: 22000, note: 'Chicken Tikka' } } },
]);
const rows = await getUserExportRows(USER, YEAR, MONTH);
// Volba "budu objednávat" se sloučí až se skupinou, kde si uživatel reálně objednal
expect(rows).toHaveLength(1);
expect(rows[0]).toMatchObject({ type: 'Budu objednávat', store: 'Namaste', food: 'Chicken Tikka', amount: 22000 });
});
test('člen skupiny, který si nic neobjednal, ale má poznámku, nemá vyplněnou částku', async () => {
await saveExtra('2025-03-12', [{
id: 'g3',
name: 'KFC',
creatorLogin: USER,
state: GroupState.ORDERED,
members: { [USER]: { note: 'beru si jen pití od kolegy' }, [OTHER]: { amount: 15000 } },
}]);
const rows = await getUserExportRows(USER, YEAR, MONTH);
expect(rows).toHaveLength(1);
// Záznam je uveden kvůli poznámce, ale částka zůstává prázdná — není co platit
expect(rows[0].orderedBy).toBe(USER);
expect(rows[0].amount).toBeUndefined();
expect(buildCsv(rows).split('\r\n')[1].split(';')[7]).toBe('');
expect(buildJson(USER, YEAR, MONTH, rows).rows[0].amount).toBeUndefined();
});
describe('computeMemberAmount', () => {
const group = (overrides: Partial<OrderGroup>): OrderGroup => ({
id: 'g',
name: 'KFC',
creatorLogin: OTHER,
state: GroupState.OPEN,
members: {},
...overrides,
});
test('neaktivní člen (nic si neobjednal) platí nula', () => {
const g = group({ shipping: 4000, members: { [USER]: {}, [OTHER]: { amount: 10000 } } });
expect(computeMemberAmount(g, USER)).toBe(0);
});
test('pevná sleva se dělí mezi aktivní členy', () => {
const g = group({
discountType: 'fixed',
discountValue: 5000,
members: { [USER]: { amount: 10000 }, [OTHER]: { amount: 10000 } },
});
expect(computeMemberAmount(g, USER)).toBe(10000 - 2500);
});
test('poplatky se rozpočítají jen mezi aktivní členy', () => {
const g = group({
fees: 1000,
shipping: 5000,
tip: 0,
members: { [USER]: { amount: 10000 }, [OTHER]: {} },
});
expect(computeMemberAmount(g, USER)).toBe(16000);
});
});
test('getUserExportFileName očistí login a doplní měsíc s příponou dle formátu', () => {
expect(getUserExportFileName('domena\\petr', 2025, 3)).toBe('luncher-domena_petr-2025-03.xlsx');
expect(getUserExportFileName('petr', 2025, 3, 'csv')).toBe('luncher-petr-2025-03.csv');
expect(getUserExportFileName('petr', 2025, 12, 'json')).toBe('luncher-petr-2025-12.json');
});
test('isExportFormat propustí jen podporované formáty', () => {
expect(isExportFormat('xlsx')).toBe(true);
expect(isExportFormat('csv')).toBe(true);
expect(isExportFormat('json')).toBe(true);
expect(isExportFormat('pdf')).toBe(false);
expect(isExportFormat(undefined)).toBe(false);
});
describe('buildCsv', () => {
const rows: UserExportRow[] = [
{ date: '2025-03-03', dayOfWeek: 'pondělí', type: 'TechTower', food: 'Svíčková', note: 'bez knedlíku' },
{
date: '2025-03-06', dayOfWeek: 'čtvrtek', type: 'Budu objednávat', food: 'Twister; s "extra" sýrem',
store: 'KFC', orderedBy: 'tomas', amount: 20000,
},
];
test('začíná BOM a hlavičkou oddělenou středníkem', () => {
const csv = buildCsv(rows);
expect(csv.charCodeAt(0)).toBe(0xFEFF);
expect(csv.slice(1).split('\r\n')[0]).toBe('"Datum";"Den";"Typ";"Vybrané jídlo";"Poznámka";"Objednávka";"Objednával";"Částka"');
});
test('formátuje datum, částku s desetinnou čárkou a escapuje uvozovky i středníky', () => {
const lines = buildCsv(rows).split('\r\n');
expect(lines[1]).toBe('"03.03.2025";"pondělí";"TechTower";"Svíčková";"bez knedlíku";;;');
expect(lines[2]).toBe('"06.03.2025";"čtvrtek";"Budu objednávat";"Twister; s ""extra"" sýrem";;"KFC";"tomas";"200,00"');
});
test('u záznamu bez objednávky nechá sloupce objednávky prázdné', () => {
const csv = buildCsv([{ date: '2025-03-03', dayOfWeek: 'pondělí', type: 'Pizza day', amount: 21000 }]);
const cells = csv.split('\r\n')[1].split(';');
expect(cells[5]).toBe('');
expect(cells[6]).toBe('');
expect(cells[7]).toBe('"210,00"');
});
});
describe('buildJson', () => {
test('obsahuje metadata, součet v korunách a jen vyplněná pole', () => {
const rows: UserExportRow[] = [
{ date: '2025-03-03', dayOfWeek: 'pondělí', type: 'SPŠE' },
{ date: '2025-03-06', dayOfWeek: 'čtvrtek', type: 'Objednávka', store: 'KFC', orderedBy: 'tomas', amount: 22000 },
];
const json = buildJson('petr', 2025, 3, rows);
expect(json).toMatchObject({ login: 'petr', year: 2025, month: 3, rowCount: 2, totalAmount: 220 });
expect(json.rows[0]).toEqual({ date: '2025-03-03', dayOfWeek: 'pondělí', type: 'SPŠE' });
expect(json.rows[1]).toEqual({
date: '2025-03-06', dayOfWeek: 'čtvrtek', type: 'Objednávka',
store: 'KFC', orderedBy: 'tomas', amount: 220,
});
});
});
describe('generateUserExport', () => {
beforeEach(async () => {
await storage.setData('2025-03-03', { choices: { SPSE: { [USER]: { note: 'polévka' } } } });
});
test('xlsx vrátí ZIP obsah se správným MIME a názvem', async () => {
const result = await generateUserExport(USER, YEAR, MONTH);
expect(result.content.subarray(0, 2).toString()).toBe('PK');
expect(result.mimeType).toContain('spreadsheetml');
expect(result.fileName).toBe('luncher-petr-2025-03.xlsx');
});
test('csv vrátí textový obsah s daty uživatele', async () => {
const result = await generateUserExport(USER, YEAR, MONTH, 'csv');
expect(result.mimeType).toBe('text/csv; charset=utf-8');
expect(result.fileName).toBe('luncher-petr-2025-03.csv');
expect(result.content.toString('utf-8')).toContain('"03.03.2025";"pondělí";"SPŠE"');
});
test('json vrátí parsovatelný obsah s metadaty', async () => {
const result = await generateUserExport(USER, YEAR, MONTH, 'json');
expect(result.mimeType).toBe('application/json; charset=utf-8');
expect(result.fileName).toBe('luncher-petr-2025-03.json');
const parsed = JSON.parse(result.content.toString('utf-8'));
expect(parsed).toMatchObject({ login: USER, year: YEAR, month: MONTH, rowCount: 1 });
expect(parsed.rows[0]).toMatchObject({ type: 'SPŠE', note: 'polévka' });
});
});