From c404a3a03ba7dd1ef8252a9877ae54114dd92956 Mon Sep 17 00:00:00 2001 From: batmanisko Date: Thu, 11 Jun 2026 12:55:21 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20spr=C3=A1vn=C3=A9=20mapov=C3=A1n=C3=AD?= =?UTF-8?q?=20stavu=20waiting=5Fdelivery=20ve=20stepperu=20Bolt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stav waiting_delivery znamená "jídlo čeká v podniku na vyzvednutí", heuristika ho ale kvůli slovu "delivery" mapovala na krok "Na cestě". - waiting_delivery (a obecně waiting_*) se nyní mapuje na "Příprava" - server nově ukládá i stav kurýra (boltCourierState z courier.state); krok "Na cestě" se aktivuje až když kurýr objednávku skutečně veze (picked_up, heading_to_client, ...), kurýr u podniku zůstává v "Příprava" - tooltip stepperu zobrazuje oba raw stavy pro snadnější diagnostiku - regresní test s reálnou odpovědí Bolt API Co-Authored-By: Claude Fable 5 --- client/src/components/BoltOrderProgress.tsx | 51 ++++++++++++++++----- client/src/pages/OrderGroupsPage.tsx | 2 +- server/src/boltTracking.ts | 7 ++- server/src/groups.ts | 3 ++ server/src/tests/boltTracking.test.ts | 24 ++++++++++ types/schemas/_index.yml | 5 +- 6 files changed, 78 insertions(+), 14 deletions(-) diff --git a/client/src/components/BoltOrderProgress.tsx b/client/src/components/BoltOrderProgress.tsx index add5144..d0435b5 100644 --- a/client/src/components/BoltOrderProgress.tsx +++ b/client/src/components/BoltOrderProgress.tsx @@ -3,16 +3,21 @@ import './BoltOrderProgress.scss'; const STEPS = ['Přijato', 'Příprava', 'Na cestě', 'Doručeno']; -/** Známé stavy objednávky z Bolt API → index kroku ve stepperu. */ -const STATE_TO_STEP: Record = { +/** + * Známé stavy objednávky z Bolt API → index kroku ve stepperu. + * Pozor: waiting_delivery znamená "jídlo čeká v podniku na vyzvednutí", + * nikoli "na cestě" — tu signalizuje až stav kurýra (picked_up apod.). + */ +const ORDER_STATE_TO_STEP: Record = { created: 0, pending: 0, accepted: 0, waiting_preparation: 0, preparing: 1, + waiting_delivery: 1, ready_for_pickup: 1, waiting_courier: 1, - courier_assigned: 1, + waiting_pickup: 1, picked_up: 2, in_delivery: 2, delivering: 2, @@ -21,32 +26,56 @@ const STATE_TO_STEP: Record = { finished: 3, }; +/** Stavy kurýra z Bolt API → index kroku. Kurýr u podniku ještě neznamená "na cestě". */ +const COURIER_STATE_TO_STEP: Record = { + heading_to_provider: 1, + arrived_to_provider: 1, + picked_up: 2, + heading_to_client: 2, + delivering: 2, + arrived_to_client: 2, + delivered: 3, +}; + /** Neznámé stavy se mapují heuristicky podle klíčových slov. */ -function stepForState(state: string): number | 'cancelled' { +function stepForOrderState(state: string): number | 'cancelled' { const s = state.toLowerCase(); - if (s in STATE_TO_STEP) return STATE_TO_STEP[s]; + if (s in ORDER_STATE_TO_STEP) return ORDER_STATE_TO_STEP[s]; if (/cancel|reject|fail/.test(s)) return 'cancelled'; if (/delivered|finished/.test(s)) return 3; - if (/courier|picked|delivery|transport/.test(s)) return 2; - if (/prepar|ready|cook/.test(s)) return 1; + if (/^waiting|prepar|ready|cook/.test(s)) return 1; + if (/picked|delivering|heading_to_client|transport/.test(s)) return 2; + return 0; +} + +function stepForCourierState(state?: string): number { + if (!state) return 0; + const s = state.toLowerCase(); + if (s in COURIER_STATE_TO_STEP) return COURIER_STATE_TO_STEP[s]; + if (/picked|client|delivering|transport/.test(s)) return 2; return 0; } interface Props { /** Raw order_state z Bolt API (např. waiting_preparation) */ state: string; + /** Raw courier.state z Bolt API (např. arrived_to_provider) */ + courierState?: string; /** Zda sledování stále běží (skupina má boltTrackingToken) */ tracking: boolean; } /** Mini progress stepper se stavem objednávky Bolt Food. */ -export default function BoltOrderProgress({ state, tracking }: Props) { - const step = stepForState(state); - if (step === 'cancelled') { +export default function BoltOrderProgress({ state, courierState, tracking }: Props) { + const orderStep = stepForOrderState(state); + if (orderStep === 'cancelled') { return Objednávka Bolt byla zrušena; } + // Stav kurýra může krok jen zpřesnit dopředu (např. waiting_delivery + picked_up → Na cestě) + const step = Math.max(orderStep, stepForCourierState(courierState)); + const rawInfo = courierState ? `${state} / kurýr: ${courierState}` : state; return ( - Stav z Bolt Food: {state}}> + Stav z Bolt Food: {rawInfo}}>
{STEPS.map((label, i) => (
diff --git a/client/src/pages/OrderGroupsPage.tsx b/client/src/pages/OrderGroupsPage.tsx index 5692237..f589b37 100644 --- a/client/src/pages/OrderGroupsPage.tsx +++ b/client/src/pages/OrderGroupsPage.tsx @@ -800,7 +800,7 @@ export default function OrderGroupsPage() { )} {group.boltOrderState && (
- +
)}
diff --git a/server/src/boltTracking.ts b/server/src/boltTracking.ts index 700e42c..222ada1 100644 --- a/server/src/boltTracking.ts +++ b/server/src/boltTracking.ts @@ -53,6 +53,7 @@ interface BoltOrder { order_id: number; order_state: string; expected_time_to_client_in_seconds?: number; + courier?: { state?: string } | null; } /** Dotáže se veřejného Bolt API na stav sdílené objednávky. Vrátí null, pokud objednávka už neexistuje. */ @@ -103,6 +104,7 @@ export async function checkBoltTracking(): Promise { for (const group of candidates) { let deliveryAt: string | undefined; let orderState: string | undefined; + let courierState: string | undefined; let clearToken = false; try { @@ -114,6 +116,7 @@ export async function checkBoltTracking(): Promise { clearToken = true; } else { orderState = order.order_state || undefined; + courierState = order.courier?.state || undefined; if (TERMINAL_STATE_REGEX.test(order.order_state ?? '')) { clearToken = true; } else if (typeof order.expected_time_to_client_in_seconds === 'number') { @@ -131,7 +134,8 @@ export async function checkBoltTracking(): Promise { const timeChanged = deliveryAt !== undefined && deliveryAt !== group.deliveryAt; const stateChanged = orderState !== undefined && orderState !== group.boltOrderState; - if (!clearToken && !timeChanged && !stateChanged) continue; + const courierChanged = courierState !== group.boltCourierState && !clearToken; + if (!clearToken && !timeChanged && !stateChanged && !courierChanged) continue; updated = await storage.updateData(key, current => { const d = current ?? data!; @@ -139,6 +143,7 @@ export async function checkBoltTracking(): Promise { if (g?.boltTrackingToken) { if (timeChanged) g.deliveryAt = deliveryAt; if (stateChanged) g.boltOrderState = orderState; + if (courierChanged) g.boltCourierState = courierState; if (clearToken) g.boltTrackingToken = undefined; } return d; diff --git a/server/src/groups.ts b/server/src/groups.ts index b189650..cec1067 100644 --- a/server/src/groups.ts +++ b/server/src/groups.ts @@ -153,6 +153,7 @@ export async function setGroupState(login: string, groupId: string, newState: Gr group.qrGenerated = undefined; group.boltTrackingToken = undefined; group.boltOrderState = undefined; + group.boltCourierState = undefined; for (const ml of memberLogins) { group.members[ml] = { ...group.members[ml], paid: undefined }; } @@ -211,6 +212,7 @@ export async function setGroupBoltTracking(login: string, groupId: string, share if (!shareUrl) { group.boltTrackingToken = undefined; group.boltOrderState = undefined; + group.boltCourierState = undefined; } else { if (group.state !== GroupState.ORDERED) throw new Error('Sledování Bolt lze nastavit pouze ve stavu "objednáno"'); const token = extractBoltToken(shareUrl); @@ -219,6 +221,7 @@ export async function setGroupBoltTracking(login: string, groupId: string, share group.boltTrackingToken = token; // Stav patří k předchozí objednávce — vyčistíme, doplní ho první poll group.boltOrderState = undefined; + group.boltCourierState = undefined; } } return saveExtraData(data, date); diff --git a/server/src/tests/boltTracking.test.ts b/server/src/tests/boltTracking.test.ts index d274b44..560102d 100644 --- a/server/src/tests/boltTracking.test.ts +++ b/server/src/tests/boltTracking.test.ts @@ -197,6 +197,30 @@ describe('checkBoltTracking', () => { 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(); diff --git a/types/schemas/_index.yml b/types/schemas/_index.yml index 8b627a9..7d3383b 100644 --- a/types/schemas/_index.yml +++ b/types/schemas/_index.yml @@ -811,7 +811,10 @@ OrderGroup: description: Token sdíleného sledování objednávky Bolt Food (poslední segment share URL). Pokud je vyplněn, server automaticky aktualizuje deliveryAt. type: string boltOrderState: - description: Poslední známý stav objednávky z Bolt API (raw order_state, např. waiting_preparation, preparing, delivered). Zůstává vyplněn i po ukončení sledování. + description: Poslední známý stav objednávky z Bolt API (raw order_state, např. waiting_preparation, waiting_delivery, delivered). Zůstává vyplněn i po ukončení sledování. + type: string + boltCourierState: + description: Poslední známý stav kurýra z Bolt API (raw courier.state, např. arrived_to_provider, picked_up). Zpřesňuje krok "Na cestě" ve stepperu. type: string # --- NEVYŘÍZENÉ QR KÓDY ---