diff --git a/client/src/components/OrderProgress.tsx b/client/src/components/OrderProgress.tsx index e0d2e56..be22565 100644 --- a/client/src/components/OrderProgress.tsx +++ b/client/src/components/OrderProgress.tsx @@ -52,9 +52,9 @@ const BOLT_COURIER_STATE_TO_STEP: Record = { /** * Známé stavy objednávky z Wolt tracking API → index kroku ve stepperu. - * Stav "ready" znamená hotové jídlo čekající na kurýra, proto zůstává v „Přípravě" - * a na „Vyzvedávání" se posune, až když je kurýr přiřazený. Že objednávku kurýr - * skutečně veze, se pozná až ze stavu kurýra 'delivering' (viz trackingProviders.ts). + * Pozor: Wolt hlásí stav podniku, ne rozvozu — "ready" (jídlo hotové) zůstává + * i celou dobu, co kurýr objednávku veze. Kroky „Vyzvedávání" a „Na cestě" proto + * neurčuje status, ale odvozený stav kurýra (viz trackingProviders.ts). */ const WOLT_STATE_TO_STEP: Record = { received: 0, @@ -70,6 +70,13 @@ const WOLT_STATE_TO_STEP: Record = { delivered: 4, }; +/** Popis odvozeného stavu kurýra u Woltu do tooltipu. */ +const WOLT_COURIER_INFO: Record = { + assigned: 'kurýr přiřazen', + delivering: 'kurýr veze objednávku', + delivering_other: 'kurýr veze objednávku, ale nejdřív doručuje jinou', +}; + /** Stavy, ve kterých už jídlo čeká připravené na kurýra. */ const PICKUP_READY_STATES: Record> = { bolt: new Set(['waiting_delivery', 'ready_for_pickup', 'waiting_courier', 'waiting_pickup']), @@ -92,8 +99,9 @@ function stepForOrderState(provider: TrackingProvider, state: string): number | function stepForCourierState(provider: TrackingProvider, state?: string): number { if (!state) return 0; const s = state.toLowerCase(); - // Wolt má jen odvozené stavy: 'assigned' o kroku nic neříká, 'delivering' = veze naši objednávku - if (provider === 'wolt') return s === 'delivering' ? 3 : 0; + // Wolt má jen odvozené stavy: 'assigned' o kroku nic neříká, 'delivering' + // i 'delivering_other' znamenají, že kurýr naši objednávku už veze + if (provider === 'wolt') return s.startsWith('delivering') ? 3 : 0; if (s in BOLT_COURIER_STATE_TO_STEP) return BOLT_COURIER_STATE_TO_STEP[s]; if (/picked|client|delivering|transport/.test(s)) return 3; return 0; @@ -127,7 +135,7 @@ export default function OrderProgress({ provider, state, courierState, tracking const foodReadyForPickup = PICKUP_READY_STATES[provider]?.has(state.toLowerCase()); if (step < 3 && courierAtProvider && foodReadyForPickup) step = 2; const courierInfo = provider === 'wolt' - ? (courierState === 'delivering' ? 'kurýr veze objednávku' : 'kurýr přiřazen') + ? WOLT_COURIER_INFO[courierState ?? ''] ?? 'kurýr přiřazen' : `kurýr: ${courierState}`; const rawInfo = courierState ? `${state} / ${courierInfo}` : state; return ( diff --git a/server/src/tests/orderTracking.test.ts b/server/src/tests/orderTracking.test.ts index 67a6bba..60e40a4 100644 --- a/server/src/tests/orderTracking.test.ts +++ b/server/src/tests/orderTracking.test.ts @@ -314,6 +314,10 @@ describe('checkOrderTracking (Wolt)', () => { return { data: { refresh_in_seconds: 30, timezone: 'Europe/Prague', ...details } }; } + /** ISO čas N minut v minulosti/budoucnosti — pickup_eta se posuzuje vůči teď. */ + const past = (minutes: number) => new Date(Date.now() - minutes * 60_000).toISOString(); + const future = (minutes: number) => new Date(Date.now() + minutes * 60_000).toISOString(); + /** 404 z Wolt API (neznámý nebo expirovaný tracking code). */ function woltNotFound() { (mockedAxios.isAxiosError as unknown as jest.Mock).mockImplementation((e: any) => !!e?.isAxiosError); @@ -364,10 +368,11 @@ describe('checkOrderTracking (Wolt)', () => { 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 + test('kurýr u podniku před vyzvednutím je jen assigned', async () => { + // Reálná odpověď z chvíle, kdy kurýr dorazil k podniku — pickup_eta teprve nastane mockedAxios.get.mockResolvedValue(woltResponse({ status: 'ready', + pickup_eta: future(10), delivery_eta: '2026-08-24T09:35:33.066000+00:00', couriers: [{ id: '31ce2f6e139bec6c', @@ -383,9 +388,22 @@ describe('checkOrderTracking (Wolt)', () => { expect(group.trackingCourierState).toBe('assigned'); }); - test('kurýr vezoucí naši objednávku se uloží jako delivering', async () => { + test('po vyzvednutí veze kurýr objednávku, i když nejdřív doručuje cizí', async () => { + // Wolt spojuje objednávky: náš oběd je v autě, kurýr se ale zatím vzdaluje k cizí adrese mockedAxios.get.mockResolvedValue(woltResponse({ status: 'ready', + pickup_eta: past(35), + delivery_eta: '2026-08-24T09:35:33.066000+00:00', + couriers: [{ is_delivering: true, is_delivering_other_order: true }], + })); + await checkOrderTracking(); + expect((await getGroup()).trackingCourierState).toBe('delivering_other'); + }); + + test('kurýr mířící k nám se uloží jako delivering', async () => { + mockedAxios.get.mockResolvedValue(woltResponse({ + status: 'ready', + pickup_eta: past(35), delivery_eta: '2026-08-24T09:35:33.066000+00:00', couriers: [{ is_delivering: true, is_delivering_other_order: false }], })); @@ -396,6 +414,7 @@ describe('checkOrderTracking (Wolt)', () => { test('kurýr bez příznaků doručování je assigned', async () => { mockedAxios.get.mockResolvedValue(woltResponse({ status: 'ready', + pickup_eta: past(35), delivery_eta: '2026-08-24T09:35:33.066000+00:00', couriers: [{ coordinates: { lat: 49.72, lon: 13.34 } }], })); diff --git a/server/src/trackingProviders.ts b/server/src/trackingProviders.ts index f839df8..1639cad 100644 --- a/server/src/trackingProviders.ts +++ b/server/src/trackingProviders.ts @@ -153,6 +153,7 @@ const boltTracker: DeliveryTracker = { interface WoltTrackingDetails { status?: string; delivery_eta?: string | null; + pickup_eta?: string | null; couriers?: WoltCourier[]; timezone?: string; } @@ -165,14 +166,25 @@ interface WoltCourier { } /** - * Wolt neposílá stav kurýra, jen jeho pozici a dva příznaky. Odvodíme z nich - * dvojí stav: 'delivering' (veze naši objednávku → krok „Na cestě") a 'assigned' - * (kurýr přiřazen, ale naše jídlo ještě nemá — typicky doručuje cizí objednávku). + * Wolt nemá stav „vyzvednuto" — status zůstává celou dobu doručování na hodnotě + * ready (ta popisuje jen podnik: jídlo je hotové). Že kurýr jídlo skutečně veze, + * poznáme podle pickup_eta: dokud vyzvednutí teprve nastane, je v budoucnosti; + * po vyzvednutí se ustálí na skutečném čase a zůstane v minulosti (na rozdíl od + * delivery_eta, který se dál přepočítává). + * + * Odvozené stavy kurýra: + * - 'assigned' kurýr přiřazen, jídlo ještě nevyzvedl + * - 'delivering' veze naši objednávku k nám + * - 'delivering_other' veze naši objednávku, ale nejdřív doručuje cizí (Wolt + * spojuje objednávky — kurýr se pak na mapě vzdaluje) */ -function woltCourierState(couriers?: WoltCourier[]): string | undefined { - const courier = couriers?.[0]; +function woltCourierState(details: WoltTrackingDetails, now: number = Date.now()): string | undefined { + const courier = details.couriers?.[0]; if (!courier) return undefined; - return courier.is_delivering && !courier.is_delivering_other_order ? 'delivering' : 'assigned'; + const pickupEta = details.pickup_eta ? new Date(details.pickup_eta).getTime() : NaN; + const pickedUp = !Number.isNaN(pickupEta) && pickupEta <= now; + if (!courier.is_delivering || !pickedUp) return 'assigned'; + return courier.is_delivering_other_order ? 'delivering_other' : 'delivering'; } const woltTracker: DeliveryTracker = { @@ -214,7 +226,7 @@ const woltTracker: DeliveryTracker = { } return { orderState: details.status, - courierState: woltCourierState(details.couriers), + courierState: woltCourierState(details), deliveryAt: details.delivery_eta ? formatEtaHHMM(details.delivery_eta, details.timezone) : undefined, }; }, diff --git a/types/schemas/_index.yml b/types/schemas/_index.yml index 008b4a6..619b2c6 100644 --- a/types/schemas/_index.yml +++ b/types/schemas/_index.yml @@ -975,7 +975,7 @@ OrderGroup: description: Poslední známý stav objednávky ze sledovací služby (raw hodnota — Bolt order_state, např. waiting_preparation; Wolt status, např. production). Zůstává vyplněn i po ukončení sledování. type: string trackingCourierState: - description: Poslední známý stav kurýra ze sledovací služby (Bolt raw courier.state, např. arrived_to_provider; Wolt odvozené "assigned"/"delivering"). Zpřesňuje krok "Na cestě" ve stepperu. + description: Poslední známý stav kurýra ze sledovací služby (Bolt raw courier.state, např. arrived_to_provider; Wolt odvozené "assigned"/"delivering"/"delivering_other"). Zpřesňuje krok "Na cestě" ve stepperu. type: string # --- NEVYŘÍZENÉ QR KÓDY ---