feat: sledování objednávek přes Wolt
CI / Generate TypeScript types (push) Successful in 13s
CI / Server unit tests (push) Successful in 25s
CI / Build server (push) Successful in 28s
CI / Build client (push) Successful in 41s
CI / Playwright E2E tests (push) Successful in 1m47s
CI / Build and push Docker image (push) Successful in 53s
CI / Notify (push) Successful in 2s
CI / Generate TypeScript types (push) Successful in 13s
CI / Server unit tests (push) Successful in 25s
CI / Build server (push) Successful in 28s
CI / Build client (push) Successful in 41s
CI / Playwright E2E tests (push) Successful in 1m47s
CI / Build and push Docker image (push) Successful in 53s
CI / Notify (push) Successful in 2s
Vedle Bolt Food jde nově sledovat i objednávka z Woltu — stačí u objednané skupiny vložit odkaz z track.wolt.com. Logika sledování je zobecněná do registru rozvozových služeb (trackingProviders.ts): každá služba umí vytáhnout kód ze sdílecího odkazu a dotázat se svého API, scheduler i stepper jsou společné. - pole skupiny bolt* přejmenována na tracking* + nové trackingProvider - endpoint /groups/setBoltTracking → /groups/setTracking - Wolt: čas doručení z delivery_eta v časové zóně objednávky, 404 ukončí sledování, stav kurýra odvozen z is_delivering/is_delivering_other_order - DEV simulace zůstává jen pro Bolt Food
This commit is contained in:
1 parent
eeb1630391
commit
5c048fe1f1
25 files changed
+1326
-869
No files matched your search
@@ -1,416 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import { resetMemoryStorage } from '../storage/memory';
|
||||
import getStorage from '../storage';
|
||||
import { addStore } from '../stores';
|
||||
import { createGroup, setGroupState, setGroupBoltTracking, addGroupMember } from '../groups';
|
||||
import { saveNotificationSettings } from '../notifikace';
|
||||
import { sendPushToLogins } from '../pushReminder';
|
||||
import { extractBoltToken, computeDeliveryHHMM, checkBoltTracking } from '../boltTracking';
|
||||
import { startBoltSimulation, advanceBoltSimulation, setBoltSimulationStep, stopBoltSimulationByGroup } from '../boltSimulator';
|
||||
import { ClientData, GroupState } from '../../../types/gen/types.gen';
|
||||
import { formatDate } from '../utils';
|
||||
|
||||
jest.mock('axios');
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
||||
|
||||
const mockEmit = jest.fn();
|
||||
jest.mock('../websocket', () => ({
|
||||
getWebsocket: () => ({ emit: mockEmit }),
|
||||
}));
|
||||
|
||||
jest.mock('../pushReminder', () => ({ sendPushToLogins: jest.fn() }));
|
||||
const mockedSendPush = sendPushToLogins as jest.MockedFunction<typeof sendPushToLogins>;
|
||||
|
||||
const storage = getStorage();
|
||||
|
||||
const CREATOR = 'tomas';
|
||||
const USER = 'petr';
|
||||
const ADMIN_PW = 'testadmin';
|
||||
const STORE = 'McDonald\'s';
|
||||
const TOKEN = '0d521a8be3c4acebb26d8bd5716d91eac67050fb152a899a55fa19bd5ed65f15';
|
||||
const SHARE_URL = `https://food.bolt.eu/sharedActiveOrder/${TOKEN}`;
|
||||
|
||||
function boltResponse(order: object | null) {
|
||||
return {
|
||||
data: {
|
||||
code: 0,
|
||||
message: 'OK',
|
||||
data: { orders: order ? [order] : [], baskets: [] },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
resetMemoryStorage();
|
||||
jest.clearAllMocks();
|
||||
process.env.ADMIN_PASSWORD = ADMIN_PW;
|
||||
await addStore(STORE, ADMIN_PW);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.ADMIN_PASSWORD;
|
||||
});
|
||||
|
||||
describe('extractBoltToken', () => {
|
||||
test('přijme plnou share URL', () => {
|
||||
expect(extractBoltToken(SHARE_URL)).toBe(TOKEN);
|
||||
});
|
||||
|
||||
test('toleruje lomítko, query a hash na konci', () => {
|
||||
expect(extractBoltToken(`${SHARE_URL}/`)).toBe(TOKEN);
|
||||
expect(extractBoltToken(`${SHARE_URL}?utm=x`)).toBe(TOKEN);
|
||||
expect(extractBoltToken(`${SHARE_URL}#sekce`)).toBe(TOKEN);
|
||||
expect(extractBoltToken(` ${SHARE_URL} `)).toBe(TOKEN);
|
||||
});
|
||||
|
||||
test('přijme samotný token včetně velkých písmen', () => {
|
||||
expect(extractBoltToken(TOKEN)).toBe(TOKEN);
|
||||
expect(extractBoltToken(TOKEN.toUpperCase())).toBe(TOKEN.toUpperCase());
|
||||
});
|
||||
|
||||
test('odmítne neplatný vstup', () => {
|
||||
expect(extractBoltToken('')).toBeNull();
|
||||
expect(extractBoltToken('nesmysl')).toBeNull();
|
||||
expect(extractBoltToken('https://food.bolt.eu/sharedActiveOrder/abc123')).toBeNull();
|
||||
expect(extractBoltToken(`https://food.bolt.eu/sharedActiveOrder/${'z'.repeat(64)}`)).toBeNull();
|
||||
expect(extractBoltToken(TOKEN.slice(0, 63))).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeDeliveryHHMM', () => {
|
||||
test('přičte sekundy k aktuálnímu času', () => {
|
||||
expect(computeDeliveryHHMM(1800, new Date('2025-01-10T11:00:00'))).toBe('11:30');
|
||||
});
|
||||
|
||||
test('přechod přes půlnoc', () => {
|
||||
expect(computeDeliveryHHMM(1200, new Date('2025-01-10T23:50:00'))).toBe('00:10');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setGroupBoltTracking', () => {
|
||||
const TODAY = new Date('2025-01-10');
|
||||
let groupId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const d = await createGroup(CREATOR, STORE, TODAY);
|
||||
groupId = d.groups![0].id;
|
||||
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||
await setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY);
|
||||
});
|
||||
|
||||
test('uloží token ze share URL', async () => {
|
||||
const d = await setGroupBoltTracking(CREATOR, groupId, SHARE_URL, TODAY);
|
||||
expect(d.groups![0].boltTrackingToken).toBe(TOKEN);
|
||||
});
|
||||
|
||||
test('prázdná hodnota sledování zruší včetně stavu', async () => {
|
||||
await setGroupBoltTracking(CREATOR, groupId, SHARE_URL, TODAY);
|
||||
const d = await setGroupBoltTracking(CREATOR, groupId, '', TODAY);
|
||||
expect(d.groups![0].boltTrackingToken).toBeUndefined();
|
||||
expect(d.groups![0].boltOrderState).toBeUndefined();
|
||||
});
|
||||
|
||||
test('nový token vynuluje stav předchozí objednávky', async () => {
|
||||
await setGroupBoltTracking(CREATOR, groupId, SHARE_URL, TODAY);
|
||||
await storage.updateData<ClientData>(`2025-01-10_extra`, (current) => {
|
||||
current!.groups![0].boltOrderState = 'preparing';
|
||||
return current!;
|
||||
});
|
||||
// Stejný token stav nemění
|
||||
let d = await setGroupBoltTracking(CREATOR, groupId, SHARE_URL, TODAY);
|
||||
expect(d.groups![0].boltOrderState).toBe('preparing');
|
||||
// Jiný token stav vynuluje
|
||||
const otherUrl = `https://food.bolt.eu/sharedActiveOrder/${'b'.repeat(64)}`;
|
||||
d = await setGroupBoltTracking(CREATOR, groupId, otherUrl, TODAY);
|
||||
expect(d.groups![0].boltOrderState).toBeUndefined();
|
||||
expect(d.groups![0].boltTrackingToken).toBe('b'.repeat(64));
|
||||
});
|
||||
|
||||
test('odmítne neplatný odkaz', async () => {
|
||||
await expect(setGroupBoltTracking(CREATOR, groupId, 'nesmysl', TODAY)).rejects.toThrow('Neplatný odkaz');
|
||||
});
|
||||
|
||||
test('nezakladatel nemůže sledování nastavit', async () => {
|
||||
await expect(setGroupBoltTracking(USER, groupId, SHARE_URL, TODAY)).rejects.toThrow('zakladatel');
|
||||
});
|
||||
|
||||
test('nelze nastavit mimo stav objednáno', async () => {
|
||||
const d = await createGroup(CREATOR, STORE, TODAY);
|
||||
const openGroupId = d.groups![1].id;
|
||||
await expect(setGroupBoltTracking(CREATOR, openGroupId, SHARE_URL, TODAY)).rejects.toThrow('objednáno');
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkBoltTracking', () => {
|
||||
// Scheduler čte vždy dnešní data (getToday), proto se skupiny zakládají bez explicitního data
|
||||
const extraKey = () => `${formatDate(new Date())}_extra`;
|
||||
let groupId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const d = await createGroup(CREATOR, STORE);
|
||||
groupId = d.groups![0].id;
|
||||
await setGroupState(CREATOR, groupId, GroupState.LOCKED);
|
||||
await setGroupState(CREATOR, groupId, GroupState.ORDERED);
|
||||
await setGroupBoltTracking(CREATOR, groupId, SHARE_URL);
|
||||
});
|
||||
|
||||
async function getGroup() {
|
||||
const data = await storage.getData<ClientData>(extraKey());
|
||||
return data!.groups!.find(g => g.id === groupId)!;
|
||||
}
|
||||
|
||||
test('aktualizuje deliveryAt podle expected_time_to_client_in_seconds', async () => {
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'waiting_preparation', expected_time_to_client_in_seconds: 1800 }));
|
||||
const before = computeDeliveryHHMM(1800);
|
||||
await checkBoltTracking();
|
||||
const after = computeDeliveryHHMM(1800);
|
||||
const group = await getGroup();
|
||||
expect([before, after]).toContain(group.deliveryAt);
|
||||
expect(group.boltOrderState).toBe('waiting_preparation');
|
||||
expect(group.boltTrackingToken).toBe(TOKEN);
|
||||
expect(mockEmit).toHaveBeenCalledWith('message', expect.anything());
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
expect.stringContaining('getOrderPolling'),
|
||||
{ token: TOKEN },
|
||||
expect.objectContaining({ headers: { 'Content-Type': 'application/json' } }),
|
||||
);
|
||||
});
|
||||
|
||||
test('nezapisuje, pokud se čas nezměnil', async () => {
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'preparing', expected_time_to_client_in_seconds: 1800 }));
|
||||
await checkBoltTracking();
|
||||
expect(mockEmit).toHaveBeenCalledTimes(1);
|
||||
await checkBoltTracking();
|
||||
expect(mockEmit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('ukončí sledování po doručení (token smazán, deliveryAt zůstává)', async () => {
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'preparing', expected_time_to_client_in_seconds: 1800 }));
|
||||
await checkBoltTracking();
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'delivered', expected_time_to_client_in_seconds: 0 }));
|
||||
await checkBoltTracking();
|
||||
const group = await getGroup();
|
||||
expect(group.boltTrackingToken).toBeUndefined();
|
||||
expect(group.boltOrderState).toBe('delivered');
|
||||
expect(group.deliveryAt).toMatch(/^\d{2}:\d{2}$/);
|
||||
});
|
||||
|
||||
test('ukončí sledování, když objednávka už neexistuje', async () => {
|
||||
mockedAxios.post.mockResolvedValue(boltResponse(null));
|
||||
await checkBoltTracking();
|
||||
const group = await getGroup();
|
||||
expect(group.boltTrackingToken).toBeUndefined();
|
||||
expect(group.boltOrderState).toBe('delivered');
|
||||
});
|
||||
|
||||
test('ukládá stav kurýra (reálná odpověď s waiting_delivery)', async () => {
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({
|
||||
order_id: 312222357,
|
||||
order_state: 'waiting_delivery',
|
||||
expected_time_to_client_in_seconds: 911,
|
||||
provider: { provider_id: 82859, state: 'waiting_pickup' },
|
||||
courier: { courier_id: 1958424, state: 'arrived_to_provider', lat: 49.7, lng: 13.3 },
|
||||
}));
|
||||
await checkBoltTracking();
|
||||
let group = await getGroup();
|
||||
expect(group.boltOrderState).toBe('waiting_delivery');
|
||||
expect(group.boltCourierState).toBe('arrived_to_provider');
|
||||
|
||||
// Kurýr vyzvedl — změní se jen courier state
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({
|
||||
order_state: 'waiting_delivery',
|
||||
expected_time_to_client_in_seconds: 911,
|
||||
courier: { state: 'picked_up' },
|
||||
}));
|
||||
await checkBoltTracking();
|
||||
group = await getGroup();
|
||||
expect(group.boltCourierState).toBe('picked_up');
|
||||
});
|
||||
|
||||
test('aktualizuje boltOrderState při změně stavu beze změny času', async () => {
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'waiting_preparation', expected_time_to_client_in_seconds: 1800 }));
|
||||
await checkBoltTracking();
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'preparing', expected_time_to_client_in_seconds: 1800 }));
|
||||
await checkBoltTracking();
|
||||
const group = await getGroup();
|
||||
expect(group.boltOrderState).toBe('preparing');
|
||||
expect(group.boltTrackingToken).toBe(TOKEN);
|
||||
});
|
||||
|
||||
test('chybová odpověď Bolt API (code != 0) se počítá jako selhání', async () => {
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
mockedAxios.post.mockResolvedValue({ data: { code: 42, message: 'FAIL' } });
|
||||
await checkBoltTracking();
|
||||
const group = await getGroup();
|
||||
expect(group.boltTrackingToken).toBe(TOKEN);
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('po 10 po sobě jdoucích selháních sledování ukončí', async () => {
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
mockedAxios.post.mockRejectedValue(new Error('network down'));
|
||||
for (let i = 0; i < 9; i++) {
|
||||
await checkBoltTracking();
|
||||
}
|
||||
expect((await getGroup()).boltTrackingToken).toBe(TOKEN);
|
||||
await checkBoltTracking();
|
||||
expect((await getGroup()).boltTrackingToken).toBeUndefined();
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('ignoruje skupiny mimo stav objednáno', async () => {
|
||||
await storage.updateData<ClientData>(extraKey(), (current) => {
|
||||
const d = current!;
|
||||
const g = d.groups!.find(x => x.id === groupId)!;
|
||||
g.state = GroupState.LOCKED;
|
||||
return d;
|
||||
});
|
||||
await checkBoltTracking();
|
||||
expect(mockedAxios.post).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DEV simulace (boltSimulator + checkBoltTracking)', () => {
|
||||
const extraKey = () => `${formatDate(new Date())}_extra`;
|
||||
let groupId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const d = await createGroup(CREATOR, STORE);
|
||||
groupId = d.groups![0].id;
|
||||
await setGroupState(CREATOR, groupId, GroupState.LOCKED);
|
||||
await setGroupState(CREATOR, groupId, GroupState.ORDERED);
|
||||
// Simulátor vygeneruje validní 64-hex token a přiřadíme ho skupině jako reálný dev endpoint
|
||||
const token = startBoltSimulation(groupId);
|
||||
await setGroupBoltTracking(CREATOR, groupId, `https://food.bolt.eu/sharedActiveOrder/${token}`);
|
||||
});
|
||||
|
||||
afterEach(() => stopBoltSimulationByGroup(groupId));
|
||||
|
||||
async function getGroup() {
|
||||
const data = await storage.getData<ClientData>(extraKey());
|
||||
return data!.groups!.find(g => g.id === groupId)!;
|
||||
}
|
||||
|
||||
test('simulovaný token nevolá reálné Bolt API', async () => {
|
||||
await checkBoltTracking();
|
||||
expect(mockedAxios.post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('první poll nastaví stav waiting_acceptance a ETA', async () => {
|
||||
await checkBoltTracking();
|
||||
const g = await getGroup();
|
||||
expect(g.boltOrderState).toBe('waiting_acceptance');
|
||||
expect(g.deliveryAt).toBe(computeDeliveryHHMM(2100));
|
||||
});
|
||||
|
||||
test('advance posune sekvenci na accepted', async () => {
|
||||
await checkBoltTracking();
|
||||
advanceBoltSimulation(groupId);
|
||||
await checkBoltTracking();
|
||||
expect((await getGroup()).boltOrderState).toBe('accepted');
|
||||
});
|
||||
|
||||
test('ruční nastavení stavu (override) se projeví při pollu', async () => {
|
||||
setBoltSimulationStep(groupId, { order_state: 'in_delivery', courier_state: 'heading_to_client', etaSeconds: 300 });
|
||||
await checkBoltTracking();
|
||||
const g = await getGroup();
|
||||
expect(g.boltOrderState).toBe('in_delivery');
|
||||
expect(g.boltCourierState).toBe('heading_to_client');
|
||||
});
|
||||
|
||||
test('terminální stav delivered ukončí sledování (smaže token)', async () => {
|
||||
setBoltSimulationStep(groupId, { order_state: 'delivered' });
|
||||
await checkBoltTracking();
|
||||
const g = await getGroup();
|
||||
expect(g.boltOrderState).toBe('delivered');
|
||||
expect(g.boltTrackingToken).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('notifikace o doručení objednávky', () => {
|
||||
const MEMBER = 'petr';
|
||||
const OTHER = 'jana';
|
||||
let groupId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const d = await createGroup(CREATOR, STORE);
|
||||
groupId = d.groups![0].id;
|
||||
await addGroupMember(CREATOR, groupId, MEMBER);
|
||||
await addGroupMember(CREATOR, groupId, OTHER);
|
||||
await setGroupState(CREATOR, groupId, GroupState.LOCKED);
|
||||
await setGroupState(CREATOR, groupId, GroupState.ORDERED);
|
||||
await setGroupBoltTracking(CREATOR, groupId, SHARE_URL);
|
||||
});
|
||||
|
||||
/** Posune objednávku do stavu "na cestě", aby další poll byl skutečný přechod. */
|
||||
async function tickInDelivery() {
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'in_delivery', expected_time_to_client_in_seconds: 600 }));
|
||||
await checkBoltTracking();
|
||||
}
|
||||
|
||||
test('doručení notifikuje členy, kteří to mají zapnuté', async () => {
|
||||
await saveNotificationSettings(MEMBER, { boltDeliveredPush: true });
|
||||
await saveNotificationSettings(OTHER, { boltDeliveredPush: false });
|
||||
await tickInDelivery();
|
||||
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'delivered' }));
|
||||
await checkBoltTracking();
|
||||
|
||||
expect(mockedSendPush).toHaveBeenCalledTimes(1);
|
||||
expect(mockedSendPush).toHaveBeenCalledWith(
|
||||
[MEMBER],
|
||||
expect.objectContaining({ body: expect.stringContaining(STORE), url: '/objednani' }),
|
||||
);
|
||||
});
|
||||
|
||||
test('zakladatel notifikaci nedostane, ani když ji má zapnutou', async () => {
|
||||
await saveNotificationSettings(CREATOR, { boltDeliveredPush: true });
|
||||
await saveNotificationSettings(MEMBER, { boltDeliveredPush: true });
|
||||
await tickInDelivery();
|
||||
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'delivered' }));
|
||||
await checkBoltTracking();
|
||||
|
||||
expect(mockedSendPush).toHaveBeenCalledWith([MEMBER], expect.anything());
|
||||
});
|
||||
|
||||
test('bez zapnutého nastavení se neposílá nic', async () => {
|
||||
await tickInDelivery();
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'delivered' }));
|
||||
await checkBoltTracking();
|
||||
expect(mockedSendPush).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('notifikace se neposílá opakovaně (delivered → finished → objednávka zmizí)', async () => {
|
||||
await saveNotificationSettings(MEMBER, { boltDeliveredPush: true });
|
||||
await tickInDelivery();
|
||||
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'delivered' }));
|
||||
await checkBoltTracking();
|
||||
expect(mockedSendPush).toHaveBeenCalledTimes(1);
|
||||
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'finished' }));
|
||||
await checkBoltTracking();
|
||||
mockedAxios.post.mockResolvedValue(boltResponse(null));
|
||||
await checkBoltTracking();
|
||||
expect(mockedSendPush).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('zmizelá objednávka bez předchozího doručení notifikuje', async () => {
|
||||
await saveNotificationSettings(MEMBER, { boltDeliveredPush: true });
|
||||
await tickInDelivery();
|
||||
|
||||
mockedAxios.post.mockResolvedValue(boltResponse(null));
|
||||
await checkBoltTracking();
|
||||
expect(mockedSendPush).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('zrušená objednávka notifikaci neposílá', async () => {
|
||||
await saveNotificationSettings(MEMBER, { boltDeliveredPush: true });
|
||||
await tickInDelivery();
|
||||
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'cancelled' }));
|
||||
await checkBoltTracking();
|
||||
expect(mockedSendPush).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { resetMemoryStorage } from '../storage/memory';
|
||||
import { getStores, addStore } from '../stores';
|
||||
import { createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState, setGroupBoltTracking, markGroupMemberPaid } from '../groups';
|
||||
import { createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState, setGroupTracking, markGroupMemberPaid } from '../groups';
|
||||
import { GroupState } from '../../../types/gen/types.gen';
|
||||
|
||||
const CREATOR = 'tomas';
|
||||
@@ -193,13 +193,14 @@ describe('setGroupState', () => {
|
||||
await expect(setGroupState(USER, groupId, GroupState.LOCKED, TODAY)).rejects.toThrow('zakladatel');
|
||||
});
|
||||
|
||||
test('ordered → locked smaže boltTrackingToken', async () => {
|
||||
test('ordered → locked smaže sledování objednávky', async () => {
|
||||
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||
await setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY);
|
||||
const token = 'a'.repeat(64);
|
||||
await setGroupBoltTracking(CREATOR, groupId, `https://food.bolt.eu/sharedActiveOrder/${token}`, TODAY);
|
||||
await setGroupTracking(CREATOR, groupId, `https://food.bolt.eu/sharedActiveOrder/${token}`, TODAY);
|
||||
const d = await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||
expect(d.groups![0].boltTrackingToken).toBeUndefined();
|
||||
expect(d.groups![0].trackingCode).toBeUndefined();
|
||||
expect(d.groups![0].trackingProvider).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,585 @@
|
||||
import axios from 'axios';
|
||||
import { resetMemoryStorage } from '../storage/memory';
|
||||
import getStorage from '../storage';
|
||||
import { addStore } from '../stores';
|
||||
import { createGroup, setGroupState, setGroupTracking, addGroupMember } from '../groups';
|
||||
import { saveNotificationSettings } from '../notifikace';
|
||||
import { sendPushToLogins } from '../pushReminder';
|
||||
import { checkOrderTracking } from '../orderTracking';
|
||||
import { extractTracking, computeDeliveryHHMM, formatEtaHHMM } from '../trackingProviders';
|
||||
import { startBoltSimulation, advanceBoltSimulation, setBoltSimulationStep, stopBoltSimulationByGroup } from '../boltSimulator';
|
||||
import { ClientData, GroupState, TrackingProvider } from '../../../types/gen/types.gen';
|
||||
import { formatDate } from '../utils';
|
||||
|
||||
jest.mock('axios');
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
||||
|
||||
const mockEmit = jest.fn();
|
||||
jest.mock('../websocket', () => ({
|
||||
getWebsocket: () => ({ emit: mockEmit }),
|
||||
}));
|
||||
|
||||
jest.mock('../pushReminder', () => ({ sendPushToLogins: jest.fn() }));
|
||||
const mockedSendPush = sendPushToLogins as jest.MockedFunction<typeof sendPushToLogins>;
|
||||
|
||||
const storage = getStorage();
|
||||
|
||||
const CREATOR = 'tomas';
|
||||
const USER = 'petr';
|
||||
const ADMIN_PW = 'testadmin';
|
||||
const STORE = 'McDonald\'s';
|
||||
const TOKEN = '0d521a8be3c4acebb26d8bd5716d91eac67050fb152a899a55fa19bd5ed65f15';
|
||||
const SHARE_URL = `https://food.bolt.eu/sharedActiveOrder/${TOKEN}`;
|
||||
const WOLT_CODE = 'ZfKWLEtm1JyB-nI0zk6S7g';
|
||||
const WOLT_URL = `https://track.wolt.com/${WOLT_CODE}`;
|
||||
|
||||
function boltResponse(order: object | null) {
|
||||
return {
|
||||
data: {
|
||||
code: 0,
|
||||
message: 'OK',
|
||||
data: { orders: order ? [order] : [], baskets: [] },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
resetMemoryStorage();
|
||||
jest.clearAllMocks();
|
||||
process.env.ADMIN_PASSWORD = ADMIN_PW;
|
||||
await addStore(STORE, ADMIN_PW);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.ADMIN_PASSWORD;
|
||||
});
|
||||
|
||||
describe('extractTracking', () => {
|
||||
const bolt = (code: string) => ({ provider: TrackingProvider.BOLT, code });
|
||||
const wolt = (code: string) => ({ provider: TrackingProvider.WOLT, code });
|
||||
|
||||
test('přijme plnou share URL Bolt Food', () => {
|
||||
expect(extractTracking(SHARE_URL)).toEqual(bolt(TOKEN));
|
||||
});
|
||||
|
||||
test('toleruje lomítko, query a hash na konci', () => {
|
||||
expect(extractTracking(`${SHARE_URL}/`)).toEqual(bolt(TOKEN));
|
||||
expect(extractTracking(`${SHARE_URL}?utm=x`)).toEqual(bolt(TOKEN));
|
||||
expect(extractTracking(`${SHARE_URL}#sekce`)).toEqual(bolt(TOKEN));
|
||||
expect(extractTracking(` ${SHARE_URL} `)).toEqual(bolt(TOKEN));
|
||||
});
|
||||
|
||||
test('přijme samotný token Bolt včetně velkých písmen', () => {
|
||||
expect(extractTracking(TOKEN)).toEqual(bolt(TOKEN));
|
||||
expect(extractTracking(TOKEN.toUpperCase())).toEqual(bolt(TOKEN.toUpperCase()));
|
||||
});
|
||||
|
||||
test('přijme odkaz na sledování Wolt i s jazykem v cestě', () => {
|
||||
expect(extractTracking(WOLT_URL)).toEqual(wolt(WOLT_CODE));
|
||||
expect(extractTracking(`https://track.wolt.com/en/${WOLT_CODE}`)).toEqual(wolt(WOLT_CODE));
|
||||
expect(extractTracking(` ${WOLT_URL}?utm=x `)).toEqual(wolt(WOLT_CODE));
|
||||
});
|
||||
|
||||
test('přijme samotný tracking code Wolt', () => {
|
||||
expect(extractTracking(WOLT_CODE)).toEqual(wolt(WOLT_CODE));
|
||||
});
|
||||
|
||||
test('odmítne neplatný vstup', () => {
|
||||
expect(extractTracking('')).toBeNull();
|
||||
expect(extractTracking('nesmysl')).toBeNull();
|
||||
expect(extractTracking('https://food.bolt.eu/sharedActiveOrder/abc123')).toBeNull();
|
||||
expect(extractTracking(`https://food.bolt.eu/sharedActiveOrder/${'z'.repeat(64)}`)).toBeNull();
|
||||
expect(extractTracking(TOKEN.slice(0, 63))).toBeNull();
|
||||
expect(extractTracking('https://track.wolt.com/cs')).toBeNull();
|
||||
// Odkaz cizí služby se stejně tvarovaným kódem nesmí projít jako Wolt
|
||||
expect(extractTracking(`https://track.example.com/${WOLT_CODE}`)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatEtaHHMM', () => {
|
||||
test('převede ISO čas do zóny objednávky', () => {
|
||||
expect(formatEtaHHMM('2026-08-24T09:35:33.066000+00:00', 'Europe/Prague')).toBe('11:35');
|
||||
expect(formatEtaHHMM('2026-08-24T09:35:33.066000+00:00', 'UTC')).toBe('09:35');
|
||||
});
|
||||
|
||||
test('půlnoc se zobrazí jako 00:00', () => {
|
||||
expect(formatEtaHHMM('2026-08-24T22:00:00Z', 'Europe/Prague')).toBe('00:00');
|
||||
});
|
||||
|
||||
test('neznámá zóna spadne na čas serveru, nesmyslný čas vrátí undefined', () => {
|
||||
expect(formatEtaHHMM('2026-08-24T09:35:00Z', 'Neznama/Zona')).toMatch(/^\d{2}:\d{2}$/);
|
||||
expect(formatEtaHHMM('nesmysl')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeDeliveryHHMM', () => {
|
||||
test('přičte sekundy k aktuálnímu času', () => {
|
||||
expect(computeDeliveryHHMM(1800, new Date('2025-01-10T11:00:00'))).toBe('11:30');
|
||||
});
|
||||
|
||||
test('přechod přes půlnoc', () => {
|
||||
expect(computeDeliveryHHMM(1200, new Date('2025-01-10T23:50:00'))).toBe('00:10');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setGroupTracking (Bolt)', () => {
|
||||
const TODAY = new Date('2025-01-10');
|
||||
let groupId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const d = await createGroup(CREATOR, STORE, TODAY);
|
||||
groupId = d.groups![0].id;
|
||||
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||
await setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY);
|
||||
});
|
||||
|
||||
test('uloží token ze share URL', async () => {
|
||||
const d = await setGroupTracking(CREATOR, groupId, SHARE_URL, TODAY);
|
||||
expect(d.groups![0].trackingCode).toBe(TOKEN);
|
||||
});
|
||||
|
||||
test('prázdná hodnota sledování zruší včetně stavu', async () => {
|
||||
await setGroupTracking(CREATOR, groupId, SHARE_URL, TODAY);
|
||||
const d = await setGroupTracking(CREATOR, groupId, '', TODAY);
|
||||
expect(d.groups![0].trackingCode).toBeUndefined();
|
||||
expect(d.groups![0].trackingOrderState).toBeUndefined();
|
||||
});
|
||||
|
||||
test('nový token vynuluje stav předchozí objednávky', async () => {
|
||||
await setGroupTracking(CREATOR, groupId, SHARE_URL, TODAY);
|
||||
await storage.updateData<ClientData>(`2025-01-10_extra`, (current) => {
|
||||
current!.groups![0].trackingOrderState = 'preparing';
|
||||
return current!;
|
||||
});
|
||||
// Stejný token stav nemění
|
||||
let d = await setGroupTracking(CREATOR, groupId, SHARE_URL, TODAY);
|
||||
expect(d.groups![0].trackingOrderState).toBe('preparing');
|
||||
// Jiný token stav vynuluje
|
||||
const otherUrl = `https://food.bolt.eu/sharedActiveOrder/${'b'.repeat(64)}`;
|
||||
d = await setGroupTracking(CREATOR, groupId, otherUrl, TODAY);
|
||||
expect(d.groups![0].trackingOrderState).toBeUndefined();
|
||||
expect(d.groups![0].trackingCode).toBe('b'.repeat(64));
|
||||
});
|
||||
|
||||
test('odmítne neplatný odkaz', async () => {
|
||||
await expect(setGroupTracking(CREATOR, groupId, 'nesmysl', TODAY)).rejects.toThrow('Neplatný odkaz');
|
||||
});
|
||||
|
||||
test('nezakladatel nemůže sledování nastavit', async () => {
|
||||
await expect(setGroupTracking(USER, groupId, SHARE_URL, TODAY)).rejects.toThrow('zakladatel');
|
||||
});
|
||||
|
||||
test('nelze nastavit mimo stav objednáno', async () => {
|
||||
const d = await createGroup(CREATOR, STORE, TODAY);
|
||||
const openGroupId = d.groups![1].id;
|
||||
await expect(setGroupTracking(CREATOR, openGroupId, SHARE_URL, TODAY)).rejects.toThrow('objednáno');
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkOrderTracking (Bolt)', () => {
|
||||
// Scheduler čte vždy dnešní data (getToday), proto se skupiny zakládají bez explicitního data
|
||||
const extraKey = () => `${formatDate(new Date())}_extra`;
|
||||
let groupId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const d = await createGroup(CREATOR, STORE);
|
||||
groupId = d.groups![0].id;
|
||||
await setGroupState(CREATOR, groupId, GroupState.LOCKED);
|
||||
await setGroupState(CREATOR, groupId, GroupState.ORDERED);
|
||||
await setGroupTracking(CREATOR, groupId, SHARE_URL);
|
||||
});
|
||||
|
||||
async function getGroup() {
|
||||
const data = await storage.getData<ClientData>(extraKey());
|
||||
return data!.groups!.find(g => g.id === groupId)!;
|
||||
}
|
||||
|
||||
test('aktualizuje deliveryAt podle expected_time_to_client_in_seconds', async () => {
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'waiting_preparation', expected_time_to_client_in_seconds: 1800 }));
|
||||
const before = computeDeliveryHHMM(1800);
|
||||
await checkOrderTracking();
|
||||
const after = computeDeliveryHHMM(1800);
|
||||
const group = await getGroup();
|
||||
expect([before, after]).toContain(group.deliveryAt);
|
||||
expect(group.trackingOrderState).toBe('waiting_preparation');
|
||||
expect(group.trackingCode).toBe(TOKEN);
|
||||
expect(mockEmit).toHaveBeenCalledWith('message', expect.anything());
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
expect.stringContaining('getOrderPolling'),
|
||||
{ token: TOKEN },
|
||||
expect.objectContaining({ headers: { 'Content-Type': 'application/json' } }),
|
||||
);
|
||||
});
|
||||
|
||||
test('nezapisuje, pokud se čas nezměnil', async () => {
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'preparing', expected_time_to_client_in_seconds: 1800 }));
|
||||
await checkOrderTracking();
|
||||
expect(mockEmit).toHaveBeenCalledTimes(1);
|
||||
await checkOrderTracking();
|
||||
expect(mockEmit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('ukončí sledování po doručení (token smazán, deliveryAt zůstává)', async () => {
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'preparing', expected_time_to_client_in_seconds: 1800 }));
|
||||
await checkOrderTracking();
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'delivered', expected_time_to_client_in_seconds: 0 }));
|
||||
await checkOrderTracking();
|
||||
const group = await getGroup();
|
||||
expect(group.trackingCode).toBeUndefined();
|
||||
expect(group.trackingOrderState).toBe('delivered');
|
||||
expect(group.deliveryAt).toMatch(/^\d{2}:\d{2}$/);
|
||||
});
|
||||
|
||||
test('ukončí sledování, když objednávka už neexistuje', async () => {
|
||||
mockedAxios.post.mockResolvedValue(boltResponse(null));
|
||||
await checkOrderTracking();
|
||||
const group = await getGroup();
|
||||
expect(group.trackingCode).toBeUndefined();
|
||||
expect(group.trackingOrderState).toBe('delivered');
|
||||
});
|
||||
|
||||
test('ukládá stav kurýra (reálná odpověď s waiting_delivery)', async () => {
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({
|
||||
order_id: 312222357,
|
||||
order_state: 'waiting_delivery',
|
||||
expected_time_to_client_in_seconds: 911,
|
||||
provider: { provider_id: 82859, state: 'waiting_pickup' },
|
||||
courier: { courier_id: 1958424, state: 'arrived_to_provider', lat: 49.7, lng: 13.3 },
|
||||
}));
|
||||
await checkOrderTracking();
|
||||
let group = await getGroup();
|
||||
expect(group.trackingOrderState).toBe('waiting_delivery');
|
||||
expect(group.trackingCourierState).toBe('arrived_to_provider');
|
||||
|
||||
// Kurýr vyzvedl — změní se jen courier state
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({
|
||||
order_state: 'waiting_delivery',
|
||||
expected_time_to_client_in_seconds: 911,
|
||||
courier: { state: 'picked_up' },
|
||||
}));
|
||||
await checkOrderTracking();
|
||||
group = await getGroup();
|
||||
expect(group.trackingCourierState).toBe('picked_up');
|
||||
});
|
||||
|
||||
test('aktualizuje trackingOrderState při změně stavu beze změny času', async () => {
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'waiting_preparation', expected_time_to_client_in_seconds: 1800 }));
|
||||
await checkOrderTracking();
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'preparing', expected_time_to_client_in_seconds: 1800 }));
|
||||
await checkOrderTracking();
|
||||
const group = await getGroup();
|
||||
expect(group.trackingOrderState).toBe('preparing');
|
||||
expect(group.trackingCode).toBe(TOKEN);
|
||||
});
|
||||
|
||||
test('chybová odpověď Bolt API (code != 0) se počítá jako selhání', async () => {
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
mockedAxios.post.mockResolvedValue({ data: { code: 42, message: 'FAIL' } });
|
||||
await checkOrderTracking();
|
||||
const group = await getGroup();
|
||||
expect(group.trackingCode).toBe(TOKEN);
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('po 10 po sobě jdoucích selháních sledování ukončí', async () => {
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
mockedAxios.post.mockRejectedValue(new Error('network down'));
|
||||
for (let i = 0; i < 9; i++) {
|
||||
await checkOrderTracking();
|
||||
}
|
||||
expect((await getGroup()).trackingCode).toBe(TOKEN);
|
||||
await checkOrderTracking();
|
||||
expect((await getGroup()).trackingCode).toBeUndefined();
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('ignoruje skupiny mimo stav objednáno', async () => {
|
||||
await storage.updateData<ClientData>(extraKey(), (current) => {
|
||||
const d = current!;
|
||||
const g = d.groups!.find(x => x.id === groupId)!;
|
||||
g.state = GroupState.LOCKED;
|
||||
return d;
|
||||
});
|
||||
await checkOrderTracking();
|
||||
expect(mockedAxios.post).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkOrderTracking (Wolt)', () => {
|
||||
const extraKey = () => `${formatDate(new Date())}_extra`;
|
||||
let groupId: string;
|
||||
|
||||
/** Odpověď Wolt tracking API — tvarem odpovídá reálnému /order-tracking-api/v1/details. */
|
||||
function woltResponse(details: object) {
|
||||
return { data: { refresh_in_seconds: 30, timezone: 'Europe/Prague', ...details } };
|
||||
}
|
||||
|
||||
/** 404 z Wolt API (neznámý nebo expirovaný tracking code). */
|
||||
function woltNotFound() {
|
||||
(mockedAxios.isAxiosError as unknown as jest.Mock).mockImplementation((e: any) => !!e?.isAxiosError);
|
||||
return Object.assign(new Error('Request failed with status code 404'), {
|
||||
isAxiosError: true,
|
||||
response: { status: 404, data: { detail: 'Not Found' } },
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const d = await createGroup(CREATOR, STORE);
|
||||
groupId = d.groups![0].id;
|
||||
await addGroupMember(CREATOR, groupId, USER);
|
||||
await setGroupState(CREATOR, groupId, GroupState.LOCKED);
|
||||
await setGroupState(CREATOR, groupId, GroupState.ORDERED);
|
||||
await setGroupTracking(CREATOR, groupId, WOLT_URL);
|
||||
});
|
||||
|
||||
async function getGroup() {
|
||||
const data = await storage.getData<ClientData>(extraKey());
|
||||
return data!.groups!.find(g => g.id === groupId)!;
|
||||
}
|
||||
|
||||
test('odkaz uloží službu i tracking code', async () => {
|
||||
const group = await getGroup();
|
||||
expect(group.trackingProvider).toBe(TrackingProvider.WOLT);
|
||||
expect(group.trackingCode).toBe(WOLT_CODE);
|
||||
});
|
||||
|
||||
test('nastaví deliveryAt z delivery_eta v zóně objednávky', async () => {
|
||||
mockedAxios.get.mockResolvedValue(woltResponse({
|
||||
status: 'production',
|
||||
delivery_eta: '2026-08-24T09:35:33.066000+00:00',
|
||||
pickup_eta: '2026-08-24T09:12:45.481000+00:00',
|
||||
couriers: [],
|
||||
}));
|
||||
await checkOrderTracking();
|
||||
const group = await getGroup();
|
||||
expect(group.deliveryAt).toBe('11:35');
|
||||
expect(group.trackingOrderState).toBe('production');
|
||||
expect(group.trackingCourierState).toBeUndefined();
|
||||
expect(mockEmit).toHaveBeenCalledWith('message', expect.anything());
|
||||
// Wolt se dotazuje GETem, Bolt API se nesmí volat vůbec
|
||||
expect(mockedAxios.get).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`/track/${WOLT_CODE}`),
|
||||
expect.objectContaining({ headers: expect.objectContaining({ Origin: 'https://track.wolt.com' }) }),
|
||||
);
|
||||
expect(mockedAxios.post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('kurýr doručující cizí objednávku je jen assigned', async () => {
|
||||
// Reálná odpověď: jídlo hotové, kurýr u podniku, ale veze ještě cizí objednávku
|
||||
mockedAxios.get.mockResolvedValue(woltResponse({
|
||||
status: 'ready',
|
||||
delivery_eta: '2026-08-24T09:35:33.066000+00:00',
|
||||
couriers: [{
|
||||
id: '31ce2f6e139bec6c',
|
||||
coordinates: { lat: 49.72954, lon: 13.34632 },
|
||||
vehicle_type: 'car',
|
||||
is_delivering: true,
|
||||
is_delivering_other_order: true,
|
||||
}],
|
||||
}));
|
||||
await checkOrderTracking();
|
||||
const group = await getGroup();
|
||||
expect(group.trackingOrderState).toBe('ready');
|
||||
expect(group.trackingCourierState).toBe('assigned');
|
||||
});
|
||||
|
||||
test('kurýr vezoucí naši objednávku se uloží jako delivering', async () => {
|
||||
mockedAxios.get.mockResolvedValue(woltResponse({
|
||||
status: 'ready',
|
||||
delivery_eta: '2026-08-24T09:35:33.066000+00:00',
|
||||
couriers: [{ is_delivering: true, is_delivering_other_order: false }],
|
||||
}));
|
||||
await checkOrderTracking();
|
||||
expect((await getGroup()).trackingCourierState).toBe('delivering');
|
||||
});
|
||||
|
||||
test('kurýr bez příznaků doručování je assigned', async () => {
|
||||
mockedAxios.get.mockResolvedValue(woltResponse({
|
||||
status: 'ready',
|
||||
delivery_eta: '2026-08-24T09:35:33.066000+00:00',
|
||||
couriers: [{ coordinates: { lat: 49.72, lon: 13.34 } }],
|
||||
}));
|
||||
await checkOrderTracking();
|
||||
expect((await getGroup()).trackingCourierState).toBe('assigned');
|
||||
});
|
||||
|
||||
test('doručení ukončí sledování a upozorní členy', async () => {
|
||||
await saveNotificationSettings(USER, { boltDeliveredPush: true });
|
||||
mockedAxios.get.mockResolvedValue(woltResponse({
|
||||
status: 'production',
|
||||
delivery_eta: '2026-08-24T09:35:33.066000+00:00',
|
||||
}));
|
||||
await checkOrderTracking();
|
||||
|
||||
mockedAxios.get.mockResolvedValue(woltResponse({ status: 'delivered' }));
|
||||
await checkOrderTracking();
|
||||
|
||||
const group = await getGroup();
|
||||
expect(group.trackingCode).toBeUndefined();
|
||||
expect(group.trackingOrderState).toBe('delivered');
|
||||
expect(group.deliveryAt).toBe('11:35');
|
||||
expect(mockedSendPush).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('neznámý tracking code (404) ukončí sledování', async () => {
|
||||
mockedAxios.get.mockRejectedValue(woltNotFound());
|
||||
await checkOrderTracking();
|
||||
const group = await getGroup();
|
||||
expect(group.trackingCode).toBeUndefined();
|
||||
});
|
||||
|
||||
test('odpověď bez stavu se počítá jako selhání, sledování pokračuje', async () => {
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
mockedAxios.get.mockResolvedValue(woltResponse({ delivery_eta: null }));
|
||||
await checkOrderTracking();
|
||||
const group = await getGroup();
|
||||
expect(group.trackingCode).toBe(WOLT_CODE);
|
||||
expect(group.trackingOrderState).toBeUndefined();
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DEV simulace (boltSimulator + checkOrderTracking)', () => {
|
||||
const extraKey = () => `${formatDate(new Date())}_extra`;
|
||||
let groupId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const d = await createGroup(CREATOR, STORE);
|
||||
groupId = d.groups![0].id;
|
||||
await setGroupState(CREATOR, groupId, GroupState.LOCKED);
|
||||
await setGroupState(CREATOR, groupId, GroupState.ORDERED);
|
||||
// Simulátor vygeneruje validní 64-hex token a přiřadíme ho skupině jako reálný dev endpoint
|
||||
const token = startBoltSimulation(groupId);
|
||||
await setGroupTracking(CREATOR, groupId, `https://food.bolt.eu/sharedActiveOrder/${token}`);
|
||||
});
|
||||
|
||||
afterEach(() => stopBoltSimulationByGroup(groupId));
|
||||
|
||||
async function getGroup() {
|
||||
const data = await storage.getData<ClientData>(extraKey());
|
||||
return data!.groups!.find(g => g.id === groupId)!;
|
||||
}
|
||||
|
||||
test('simulovaný token nevolá reálné Bolt API', async () => {
|
||||
await checkOrderTracking();
|
||||
expect(mockedAxios.post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('první poll nastaví stav waiting_acceptance a ETA', async () => {
|
||||
await checkOrderTracking();
|
||||
const g = await getGroup();
|
||||
expect(g.trackingOrderState).toBe('waiting_acceptance');
|
||||
expect(g.deliveryAt).toBe(computeDeliveryHHMM(2100));
|
||||
});
|
||||
|
||||
test('advance posune sekvenci na accepted', async () => {
|
||||
await checkOrderTracking();
|
||||
advanceBoltSimulation(groupId);
|
||||
await checkOrderTracking();
|
||||
expect((await getGroup()).trackingOrderState).toBe('accepted');
|
||||
});
|
||||
|
||||
test('ruční nastavení stavu (override) se projeví při pollu', async () => {
|
||||
setBoltSimulationStep(groupId, { order_state: 'in_delivery', courier_state: 'heading_to_client', etaSeconds: 300 });
|
||||
await checkOrderTracking();
|
||||
const g = await getGroup();
|
||||
expect(g.trackingOrderState).toBe('in_delivery');
|
||||
expect(g.trackingCourierState).toBe('heading_to_client');
|
||||
});
|
||||
|
||||
test('terminální stav delivered ukončí sledování (smaže token)', async () => {
|
||||
setBoltSimulationStep(groupId, { order_state: 'delivered' });
|
||||
await checkOrderTracking();
|
||||
const g = await getGroup();
|
||||
expect(g.trackingOrderState).toBe('delivered');
|
||||
expect(g.trackingCode).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('notifikace o doručení objednávky', () => {
|
||||
const MEMBER = 'petr';
|
||||
const OTHER = 'jana';
|
||||
let groupId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const d = await createGroup(CREATOR, STORE);
|
||||
groupId = d.groups![0].id;
|
||||
await addGroupMember(CREATOR, groupId, MEMBER);
|
||||
await addGroupMember(CREATOR, groupId, OTHER);
|
||||
await setGroupState(CREATOR, groupId, GroupState.LOCKED);
|
||||
await setGroupState(CREATOR, groupId, GroupState.ORDERED);
|
||||
await setGroupTracking(CREATOR, groupId, SHARE_URL);
|
||||
});
|
||||
|
||||
/** Posune objednávku do stavu "na cestě", aby další poll byl skutečný přechod. */
|
||||
async function tickInDelivery() {
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'in_delivery', expected_time_to_client_in_seconds: 600 }));
|
||||
await checkOrderTracking();
|
||||
}
|
||||
|
||||
test('doručení notifikuje členy, kteří to mají zapnuté', async () => {
|
||||
await saveNotificationSettings(MEMBER, { boltDeliveredPush: true });
|
||||
await saveNotificationSettings(OTHER, { boltDeliveredPush: false });
|
||||
await tickInDelivery();
|
||||
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'delivered' }));
|
||||
await checkOrderTracking();
|
||||
|
||||
expect(mockedSendPush).toHaveBeenCalledTimes(1);
|
||||
expect(mockedSendPush).toHaveBeenCalledWith(
|
||||
[MEMBER],
|
||||
expect.objectContaining({ body: expect.stringContaining(STORE), url: '/objednani' }),
|
||||
);
|
||||
});
|
||||
|
||||
test('zakladatel notifikaci nedostane, ani když ji má zapnutou', async () => {
|
||||
await saveNotificationSettings(CREATOR, { boltDeliveredPush: true });
|
||||
await saveNotificationSettings(MEMBER, { boltDeliveredPush: true });
|
||||
await tickInDelivery();
|
||||
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'delivered' }));
|
||||
await checkOrderTracking();
|
||||
|
||||
expect(mockedSendPush).toHaveBeenCalledWith([MEMBER], expect.anything());
|
||||
});
|
||||
|
||||
test('bez zapnutého nastavení se neposílá nic', async () => {
|
||||
await tickInDelivery();
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'delivered' }));
|
||||
await checkOrderTracking();
|
||||
expect(mockedSendPush).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('notifikace se neposílá opakovaně (delivered → finished → objednávka zmizí)', async () => {
|
||||
await saveNotificationSettings(MEMBER, { boltDeliveredPush: true });
|
||||
await tickInDelivery();
|
||||
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'delivered' }));
|
||||
await checkOrderTracking();
|
||||
expect(mockedSendPush).toHaveBeenCalledTimes(1);
|
||||
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'finished' }));
|
||||
await checkOrderTracking();
|
||||
mockedAxios.post.mockResolvedValue(boltResponse(null));
|
||||
await checkOrderTracking();
|
||||
expect(mockedSendPush).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('zmizelá objednávka bez předchozího doručení notifikuje', async () => {
|
||||
await saveNotificationSettings(MEMBER, { boltDeliveredPush: true });
|
||||
await tickInDelivery();
|
||||
|
||||
mockedAxios.post.mockResolvedValue(boltResponse(null));
|
||||
await checkOrderTracking();
|
||||
expect(mockedSendPush).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('zrušená objednávka notifikaci neposílá', async () => {
|
||||
await saveNotificationSettings(MEMBER, { boltDeliveredPush: true });
|
||||
await tickInDelivery();
|
||||
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'cancelled' }));
|
||||
await checkOrderTracking();
|
||||
expect(mockedSendPush).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user