Fix wash start recovery from API failures
This commit is contained in:
@@ -48,10 +48,13 @@ export const getDepartmentDescription = (id) => {
|
||||
|
||||
export const getDepartmentsGuest = async (queryParams = {}) => {
|
||||
isLoading.value = true;
|
||||
const queryString = new URLSearchParams(queryParams).toString();
|
||||
const request = await unauthenticatedRequest("/guest/departments" + (queryString ? `?${queryString}` : ""), "get");
|
||||
departments.value = sortByDepartmentPriorityOrder(request.data.data || []);
|
||||
isLoading.value = false;
|
||||
return departments.value;
|
||||
try {
|
||||
const queryString = new URLSearchParams(queryParams).toString();
|
||||
const request = await unauthenticatedRequest("/guest/departments" + (queryString ? `?${queryString}` : ""), "get");
|
||||
departments.value = sortByDepartmentPriorityOrder(request.data.data || []);
|
||||
return departments.value;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -2,12 +2,17 @@ import { computed, nextTick, onUnmounted, ref, watch } from "vue";
|
||||
import { getDepartmentsGuest } from "@/components/pagination/departmentTabs.vue";
|
||||
import { locations } from "@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue";
|
||||
|
||||
const isLaneSelfServeEnabled = (lane) => !(
|
||||
lane?.selfserve_enabled === false
|
||||
|| lane?.selfserve_enabled === 0
|
||||
|| lane?.selfserve_enabled === "0"
|
||||
|| ["false", "off", "no"].includes(String(lane?.selfserve_enabled ?? "").trim().toLowerCase())
|
||||
);
|
||||
const isLaneSelfServeEnabled = (lane) =>
|
||||
!(
|
||||
lane?.selfserve_enabled === false ||
|
||||
lane?.selfserve_enabled === 0 ||
|
||||
lane?.selfserve_enabled === "0" ||
|
||||
["false", "off", "no"].includes(
|
||||
String(lane?.selfserve_enabled ?? "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
)
|
||||
);
|
||||
|
||||
const toDepartmentViewModel = (department, distance = null) => ({
|
||||
id: department.id,
|
||||
@@ -18,12 +23,22 @@ const toDepartmentViewModel = (department, distance = null) => ({
|
||||
self_serve_enabled: department.self_serve_enabled,
|
||||
});
|
||||
|
||||
const extractDepartmentFetchErrorMessage = (error) => {
|
||||
const candidates = [
|
||||
error?.response?.data?.data?.message,
|
||||
error?.response?.data?.message,
|
||||
error?.response?.data?.error,
|
||||
error?.message,
|
||||
];
|
||||
|
||||
return (
|
||||
candidates.find((candidate) => typeof candidate === "string" && candidate.trim() !== "") ||
|
||||
"Kunne ikke hente vaskeafdelinger. Prøv igen."
|
||||
);
|
||||
};
|
||||
|
||||
export function useWashDepartments(options = {}) {
|
||||
const {
|
||||
includeLanes = false,
|
||||
refreshIntervalMs = 0,
|
||||
canAccessSuperUser = () => false,
|
||||
} = options;
|
||||
const { includeLanes = false, refreshIntervalMs = 0, canAccessSuperUser = () => false } = options;
|
||||
|
||||
const guestDepartments = ref([]);
|
||||
const nearestDepartment = ref(null);
|
||||
@@ -31,6 +46,7 @@ export function useWashDepartments(options = {}) {
|
||||
const forceNearestDepartmentEvaluationId = ref(0);
|
||||
const isSearchingDepartments = ref(false);
|
||||
const lastDepartmentFetchTime = ref(null);
|
||||
const departmentFetchError = ref(null);
|
||||
|
||||
let refreshInterval = null;
|
||||
|
||||
@@ -57,13 +73,18 @@ export function useWashDepartments(options = {}) {
|
||||
return nearestDepartment.value.self_serve_enabled === true;
|
||||
});
|
||||
|
||||
const buildGuestDepartmentParams = () => (
|
||||
includeLanes ? { include_lanes: true } : {}
|
||||
);
|
||||
const buildGuestDepartmentParams = () => (includeLanes ? { include_lanes: true } : {});
|
||||
|
||||
const fetchDepartments = async () => {
|
||||
guestDepartments.value = await getDepartmentsGuest(buildGuestDepartmentParams());
|
||||
lastDepartmentFetchTime.value = Date.now();
|
||||
try {
|
||||
guestDepartments.value = await getDepartmentsGuest(buildGuestDepartmentParams());
|
||||
lastDepartmentFetchTime.value = Date.now();
|
||||
departmentFetchError.value = null;
|
||||
} catch (error) {
|
||||
departmentFetchError.value = extractDepartmentFetchErrorMessage(error);
|
||||
console.warn("Failed to fetch wash departments:", error);
|
||||
}
|
||||
|
||||
evaluateLocationDepartments(locations.location.value);
|
||||
return guestDepartments.value;
|
||||
};
|
||||
@@ -74,19 +95,15 @@ export function useWashDepartments(options = {}) {
|
||||
}
|
||||
|
||||
const currentTime = Date.now();
|
||||
if (
|
||||
!lastDepartmentFetchTime.value
|
||||
|| (currentTime - lastDepartmentFetchTime.value) >= refreshIntervalMs
|
||||
) {
|
||||
if (!lastDepartmentFetchTime.value || currentTime - lastDepartmentFetchTime.value >= refreshIntervalMs) {
|
||||
return fetchDepartments();
|
||||
}
|
||||
|
||||
return guestDepartments.value;
|
||||
};
|
||||
|
||||
const getForcedDepartment = () => guestDepartments.value.find(
|
||||
(department) => department.id === forceNearestDepartmentEvaluationId.value
|
||||
);
|
||||
const getForcedDepartment = () =>
|
||||
guestDepartments.value.find((department) => department.id === forceNearestDepartmentEvaluationId.value);
|
||||
|
||||
const evaluateLocationDepartments = (locationValue = locations.location.value) => {
|
||||
if (isForcingNearestDepartment.value && forceNearestDepartmentEvaluationId.value) {
|
||||
@@ -191,19 +208,30 @@ export function useWashDepartments(options = {}) {
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => guestDepartments.value, () => {
|
||||
evaluateLocationDepartments(locations.location.value);
|
||||
}, { deep: true });
|
||||
|
||||
watch(() => locations.location.value, (newValue) => {
|
||||
evaluateLocationDepartments(newValue);
|
||||
}, { deep: true });
|
||||
|
||||
watch(() => isSearchingDepartments.value, (isSearching) => {
|
||||
if (!isSearching) {
|
||||
watch(
|
||||
() => guestDepartments.value,
|
||||
() => {
|
||||
evaluateLocationDepartments(locations.location.value);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => locations.location.value,
|
||||
(newValue) => {
|
||||
evaluateLocationDepartments(newValue);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => isSearchingDepartments.value,
|
||||
(isSearching) => {
|
||||
if (!isSearching) {
|
||||
evaluateLocationDepartments(locations.location.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAutoRefresh();
|
||||
@@ -216,6 +244,7 @@ export function useWashDepartments(options = {}) {
|
||||
forceNearestDepartmentEvaluationId,
|
||||
isSearchingDepartments,
|
||||
lastDepartmentFetchTime,
|
||||
departmentFetchError,
|
||||
availableProductIds,
|
||||
doesCurrentDepartmentSelectionHaveSelfServeEnabled,
|
||||
fetchDepartments,
|
||||
|
||||
@@ -83,6 +83,7 @@ const {
|
||||
isForcingNearestDepartment,
|
||||
forceNearestDepartmentEvaluationId,
|
||||
isSearchingDepartments,
|
||||
departmentFetchError,
|
||||
availableProductIds,
|
||||
doesCurrentDepartmentSelectionHaveSelfServeEnabled,
|
||||
fetchDepartments,
|
||||
@@ -436,6 +437,8 @@ const extractErrorMessage = (error: any, fallback: string) => {
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const getRequestStatus = (error: any) => Number(error?.response?.status ?? error?.status ?? 0);
|
||||
|
||||
watch(dynamicImageUrl, () => {
|
||||
hideDynamicImage.value = false;
|
||||
});
|
||||
@@ -779,7 +782,17 @@ const fetchServerActiveWash = async () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = await SessionUser.request(SERVER_ACTIVE_WASH_ENDPOINT, "GET");
|
||||
let response = null;
|
||||
try {
|
||||
response = await SessionUser.request(SERVER_ACTIVE_WASH_ENDPOINT, "GET");
|
||||
} catch (error) {
|
||||
if (getRequestStatus(error) === 404) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const details = unwrapApiData(response);
|
||||
if (!isAuthenticatedCustomerActiveWash(details, customerNumber)) {
|
||||
return null;
|
||||
@@ -848,7 +861,10 @@ const applyServerActiveWash = async (activeWash: any) => {
|
||||
|
||||
const summary = await fetchWashSummary(summaryParams, false);
|
||||
if (!summary) {
|
||||
await fetchSelfServeDataInternal(departmentId || nearestDepartment.value?.id, selectedVehicleTypeId, laneId, reg);
|
||||
const fallbackDepartmentId = departmentId || nearestDepartment.value?.id;
|
||||
if (fallbackDepartmentId) {
|
||||
await fetchSelfServeDataInternal(fallbackDepartmentId, selectedVehicleTypeId, laneId, reg);
|
||||
}
|
||||
}
|
||||
|
||||
saveProgress("serverActiveWash");
|
||||
@@ -1439,6 +1455,28 @@ watch(
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<b-message
|
||||
v-if="departmentFetchError"
|
||||
type="is-warning"
|
||||
has-icon
|
||||
:closable="false"
|
||||
data-testid="self-serve-departments-error"
|
||||
>
|
||||
<div class="is-flex is-align-items-center is-justify-content-space-between is-flex-wrap-wrap">
|
||||
<span class="mr-3">{{ departmentFetchError }}</span>
|
||||
<b-button
|
||||
size="is-small"
|
||||
type="is-warning is-light"
|
||||
icon-pack="fas"
|
||||
icon-left="sync-alt"
|
||||
data-testid="self-serve-departments-retry"
|
||||
@click="fetchDepartments"
|
||||
>
|
||||
{{ $t("common.try_again") }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-message>
|
||||
|
||||
<b-message
|
||||
v-if="selfServeDataError"
|
||||
type="is-danger"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @vitest-environment jsdom
|
||||
import { defineComponent, h, nextTick } from "vue";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { nextTick } from "vue";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
@@ -27,27 +27,30 @@ vi.mock("@/components/displays/department/pos/steps/mobile/objects/PosDepartment
|
||||
},
|
||||
}));
|
||||
|
||||
import { useWashDepartments } from "@/composables/useWashDepartments.js";
|
||||
const { useWashDepartments } = await import("@/composables/useWashDepartments.js");
|
||||
|
||||
const mountComposable = (factory) => {
|
||||
let result;
|
||||
const mountedWrappers = [];
|
||||
|
||||
mount(
|
||||
defineComponent({
|
||||
setup() {
|
||||
result = factory();
|
||||
return () => h("div");
|
||||
},
|
||||
})
|
||||
);
|
||||
const mountDepartments = (options = {}) => {
|
||||
let departments = null;
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
departments = useWashDepartments(options);
|
||||
return {};
|
||||
},
|
||||
template: "<div />",
|
||||
});
|
||||
mountedWrappers.push(wrapper);
|
||||
|
||||
return result;
|
||||
return departments;
|
||||
};
|
||||
|
||||
describe("useWashDepartments", () => {
|
||||
beforeEach(() => {
|
||||
mountedWrappers.splice(0).forEach((wrapper) => wrapper.unmount());
|
||||
mocks.getDepartmentsGuest.mockReset();
|
||||
mocks.getDistance.mockReset();
|
||||
vi.restoreAllMocks();
|
||||
mocks.locationRef.value = {
|
||||
coords: {
|
||||
latitude: 55.5,
|
||||
@@ -88,7 +91,7 @@ describe("useWashDepartments", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const departments = mountComposable(() => useWashDepartments({ includeLanes: true }));
|
||||
const departments = mountDepartments({ includeLanes: true });
|
||||
|
||||
await departments.fetchDepartments();
|
||||
|
||||
@@ -104,11 +107,9 @@ describe("useWashDepartments", () => {
|
||||
{ id: 2, name: "South", address: "B", latitude: 55.7, longitude: 12.6, lanes: [], self_serve_enabled: true },
|
||||
]);
|
||||
|
||||
const departments = mountComposable(() =>
|
||||
useWashDepartments({
|
||||
canAccessSuperUser: () => true,
|
||||
})
|
||||
);
|
||||
const departments = mountDepartments({
|
||||
canAccessSuperUser: () => true,
|
||||
});
|
||||
|
||||
await departments.fetchDepartments();
|
||||
departments.startDepartmentSearch();
|
||||
@@ -125,4 +126,43 @@ describe("useWashDepartments", () => {
|
||||
expect(departments.forceNearestDepartmentEvaluationId.value).toBe(0);
|
||||
expect(departments.nearestDepartment.value).toMatchObject({ id: 2, name: "South" });
|
||||
});
|
||||
|
||||
it("keeps the wash start page usable when the guest departments request fails", async () => {
|
||||
vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
mocks.getDepartmentsGuest.mockRejectedValueOnce(new Error("Network Error"));
|
||||
const departments = mountDepartments({ includeLanes: true });
|
||||
|
||||
await expect(departments.fetchDepartments()).resolves.toEqual([]);
|
||||
|
||||
expect(departments.guestDepartments.value).toEqual([]);
|
||||
expect(departments.nearestDepartment.value).toBeNull();
|
||||
expect(departments.departmentFetchError.value).toBe("Network Error");
|
||||
expect(mocks.getDepartmentsGuest).toHaveBeenCalledWith({ include_lanes: true });
|
||||
});
|
||||
|
||||
it("clears the department fetch error after a successful retry", async () => {
|
||||
vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const department = {
|
||||
id: 3,
|
||||
name: "Glostrup",
|
||||
address: "Fabriksparken 18",
|
||||
latitude: 55.6,
|
||||
longitude: 12.5,
|
||||
self_serve_enabled: true,
|
||||
lanes: [{ id: 7, status: "AVAILABLE", selfserve_enabled: true }],
|
||||
};
|
||||
mocks.getDepartmentsGuest.mockRejectedValueOnce(new Error("Gateway Timeout")).mockResolvedValueOnce([department]);
|
||||
const departments = mountDepartments({ includeLanes: true });
|
||||
|
||||
await departments.fetchDepartments();
|
||||
await departments.fetchDepartments();
|
||||
|
||||
expect(departments.departmentFetchError.value).toBeNull();
|
||||
expect(departments.guestDepartments.value).toEqual([department]);
|
||||
expect(departments.nearestDepartment.value).toMatchObject({
|
||||
id: 3,
|
||||
name: "Glostrup",
|
||||
lanes: [{ id: 7 }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user