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>(isoDate, { choices, pizzaDay }); } /** Uloží data slotu extra s předanými skupinami. */ async function saveExtra(isoDate: string, groups: OrderGroup[]) { await storage.setData>(`${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 => ({ 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' }); }); });