fix: správné mapování stavu waiting_delivery ve stepperu Bolt
CI / Generate TypeScript types (push) Successful in 12s
CI / Server unit tests (push) Successful in 20s
CI / Build server (push) Successful in 28s
CI / Build client (push) Successful in 1m10s
CI / Playwright E2E tests (push) Successful in 1m22s
CI / Build and push Docker image (push) Successful in 42s
CI / Notify (push) Successful in 1s
CI / Generate TypeScript types (push) Successful in 12s
CI / Server unit tests (push) Successful in 20s
CI / Build server (push) Successful in 28s
CI / Build client (push) Successful in 1m10s
CI / Playwright E2E tests (push) Successful in 1m22s
CI / Build and push Docker image (push) Successful in 42s
CI / Notify (push) Successful in 1s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -3,16 +3,21 @@ import './BoltOrderProgress.scss';
|
|||||||
|
|
||||||
const STEPS = ['Přijato', 'Příprava', 'Na cestě', 'Doručeno'];
|
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<string, number> = {
|
* 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<string, number> = {
|
||||||
created: 0,
|
created: 0,
|
||||||
pending: 0,
|
pending: 0,
|
||||||
accepted: 0,
|
accepted: 0,
|
||||||
waiting_preparation: 0,
|
waiting_preparation: 0,
|
||||||
preparing: 1,
|
preparing: 1,
|
||||||
|
waiting_delivery: 1,
|
||||||
ready_for_pickup: 1,
|
ready_for_pickup: 1,
|
||||||
waiting_courier: 1,
|
waiting_courier: 1,
|
||||||
courier_assigned: 1,
|
waiting_pickup: 1,
|
||||||
picked_up: 2,
|
picked_up: 2,
|
||||||
in_delivery: 2,
|
in_delivery: 2,
|
||||||
delivering: 2,
|
delivering: 2,
|
||||||
@@ -21,32 +26,56 @@ const STATE_TO_STEP: Record<string, number> = {
|
|||||||
finished: 3,
|
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<string, number> = {
|
||||||
|
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. */
|
/** 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();
|
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 (/cancel|reject|fail/.test(s)) return 'cancelled';
|
||||||
if (/delivered|finished/.test(s)) return 3;
|
if (/delivered|finished/.test(s)) return 3;
|
||||||
if (/courier|picked|delivery|transport/.test(s)) return 2;
|
if (/^waiting|prepar|ready|cook/.test(s)) return 1;
|
||||||
if (/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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/** Raw order_state z Bolt API (např. waiting_preparation) */
|
/** Raw order_state z Bolt API (např. waiting_preparation) */
|
||||||
state: string;
|
state: string;
|
||||||
|
/** Raw courier.state z Bolt API (např. arrived_to_provider) */
|
||||||
|
courierState?: string;
|
||||||
/** Zda sledování stále běží (skupina má boltTrackingToken) */
|
/** Zda sledování stále běží (skupina má boltTrackingToken) */
|
||||||
tracking: boolean;
|
tracking: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Mini progress stepper se stavem objednávky Bolt Food. */
|
/** Mini progress stepper se stavem objednávky Bolt Food. */
|
||||||
export default function BoltOrderProgress({ state, tracking }: Props) {
|
export default function BoltOrderProgress({ state, courierState, tracking }: Props) {
|
||||||
const step = stepForState(state);
|
const orderStep = stepForOrderState(state);
|
||||||
if (step === 'cancelled') {
|
if (orderStep === 'cancelled') {
|
||||||
return <small className="text-danger">Objednávka Bolt byla zrušena</small>;
|
return <small className="text-danger">Objednávka Bolt byla zrušena</small>;
|
||||||
}
|
}
|
||||||
|
// 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 (
|
return (
|
||||||
<OverlayTrigger overlay={<Tooltip>Stav z Bolt Food: {state}</Tooltip>}>
|
<OverlayTrigger overlay={<Tooltip>Stav z Bolt Food: {rawInfo}</Tooltip>}>
|
||||||
<div className={`bolt-progress${tracking && step < 3 ? ' live' : ''}`}>
|
<div className={`bolt-progress${tracking && step < 3 ? ' live' : ''}`}>
|
||||||
{STEPS.map((label, i) => (
|
{STEPS.map((label, i) => (
|
||||||
<div key={label} className={`bolt-step${i <= step ? ' done' : ''}${i === step ? ' active' : ''}`}>
|
<div key={label} className={`bolt-step${i <= step ? ' done' : ''}${i === step ? ' active' : ''}`}>
|
||||||
|
|||||||
@@ -800,7 +800,7 @@ export default function OrderGroupsPage() {
|
|||||||
)}
|
)}
|
||||||
{group.boltOrderState && (
|
{group.boltOrderState && (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<BoltOrderProgress state={group.boltOrderState} tracking={!!group.boltTrackingToken} />
|
<BoltOrderProgress state={group.boltOrderState} courierState={group.boltCourierState} tracking={!!group.boltTrackingToken} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ interface BoltOrder {
|
|||||||
order_id: number;
|
order_id: number;
|
||||||
order_state: string;
|
order_state: string;
|
||||||
expected_time_to_client_in_seconds?: number;
|
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. */
|
/** 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<void> {
|
|||||||
for (const group of candidates) {
|
for (const group of candidates) {
|
||||||
let deliveryAt: string | undefined;
|
let deliveryAt: string | undefined;
|
||||||
let orderState: string | undefined;
|
let orderState: string | undefined;
|
||||||
|
let courierState: string | undefined;
|
||||||
let clearToken = false;
|
let clearToken = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -114,6 +116,7 @@ export async function checkBoltTracking(): Promise<void> {
|
|||||||
clearToken = true;
|
clearToken = true;
|
||||||
} else {
|
} else {
|
||||||
orderState = order.order_state || undefined;
|
orderState = order.order_state || undefined;
|
||||||
|
courierState = order.courier?.state || undefined;
|
||||||
if (TERMINAL_STATE_REGEX.test(order.order_state ?? '')) {
|
if (TERMINAL_STATE_REGEX.test(order.order_state ?? '')) {
|
||||||
clearToken = true;
|
clearToken = true;
|
||||||
} else if (typeof order.expected_time_to_client_in_seconds === 'number') {
|
} else if (typeof order.expected_time_to_client_in_seconds === 'number') {
|
||||||
@@ -131,7 +134,8 @@ export async function checkBoltTracking(): Promise<void> {
|
|||||||
|
|
||||||
const timeChanged = deliveryAt !== undefined && deliveryAt !== group.deliveryAt;
|
const timeChanged = deliveryAt !== undefined && deliveryAt !== group.deliveryAt;
|
||||||
const stateChanged = orderState !== undefined && orderState !== group.boltOrderState;
|
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<ClientData>(key, current => {
|
updated = await storage.updateData<ClientData>(key, current => {
|
||||||
const d = current ?? data!;
|
const d = current ?? data!;
|
||||||
@@ -139,6 +143,7 @@ export async function checkBoltTracking(): Promise<void> {
|
|||||||
if (g?.boltTrackingToken) {
|
if (g?.boltTrackingToken) {
|
||||||
if (timeChanged) g.deliveryAt = deliveryAt;
|
if (timeChanged) g.deliveryAt = deliveryAt;
|
||||||
if (stateChanged) g.boltOrderState = orderState;
|
if (stateChanged) g.boltOrderState = orderState;
|
||||||
|
if (courierChanged) g.boltCourierState = courierState;
|
||||||
if (clearToken) g.boltTrackingToken = undefined;
|
if (clearToken) g.boltTrackingToken = undefined;
|
||||||
}
|
}
|
||||||
return d;
|
return d;
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ export async function setGroupState(login: string, groupId: string, newState: Gr
|
|||||||
group.qrGenerated = undefined;
|
group.qrGenerated = undefined;
|
||||||
group.boltTrackingToken = undefined;
|
group.boltTrackingToken = undefined;
|
||||||
group.boltOrderState = undefined;
|
group.boltOrderState = undefined;
|
||||||
|
group.boltCourierState = undefined;
|
||||||
for (const ml of memberLogins) {
|
for (const ml of memberLogins) {
|
||||||
group.members[ml] = { ...group.members[ml], paid: undefined };
|
group.members[ml] = { ...group.members[ml], paid: undefined };
|
||||||
}
|
}
|
||||||
@@ -211,6 +212,7 @@ export async function setGroupBoltTracking(login: string, groupId: string, share
|
|||||||
if (!shareUrl) {
|
if (!shareUrl) {
|
||||||
group.boltTrackingToken = undefined;
|
group.boltTrackingToken = undefined;
|
||||||
group.boltOrderState = undefined;
|
group.boltOrderState = undefined;
|
||||||
|
group.boltCourierState = undefined;
|
||||||
} else {
|
} else {
|
||||||
if (group.state !== GroupState.ORDERED) throw new Error('Sledování Bolt lze nastavit pouze ve stavu "objednáno"');
|
if (group.state !== GroupState.ORDERED) throw new Error('Sledování Bolt lze nastavit pouze ve stavu "objednáno"');
|
||||||
const token = extractBoltToken(shareUrl);
|
const token = extractBoltToken(shareUrl);
|
||||||
@@ -219,6 +221,7 @@ export async function setGroupBoltTracking(login: string, groupId: string, share
|
|||||||
group.boltTrackingToken = token;
|
group.boltTrackingToken = token;
|
||||||
// Stav patří k předchozí objednávce — vyčistíme, doplní ho první poll
|
// Stav patří k předchozí objednávce — vyčistíme, doplní ho první poll
|
||||||
group.boltOrderState = undefined;
|
group.boltOrderState = undefined;
|
||||||
|
group.boltCourierState = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return saveExtraData(data, date);
|
return saveExtraData(data, date);
|
||||||
|
|||||||
@@ -197,6 +197,30 @@ describe('checkBoltTracking', () => {
|
|||||||
expect(group.boltOrderState).toBe('delivered');
|
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 () => {
|
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 }));
|
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'waiting_preparation', expected_time_to_client_in_seconds: 1800 }));
|
||||||
await checkBoltTracking();
|
await checkBoltTracking();
|
||||||
|
|||||||
@@ -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.
|
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
|
type: string
|
||||||
boltOrderState:
|
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
|
type: string
|
||||||
|
|
||||||
# --- NEVYŘÍZENÉ QR KÓDY ---
|
# --- NEVYŘÍZENÉ QR KÓDY ---
|
||||||
|
|||||||
Reference in New Issue
Block a user