Add self-serve session handling and lane selection normalization

This commit is contained in:
Jeppe Bundgaard
2026-06-03 22:07:47 +02:00
parent af5514c982
commit da0c699b88
6 changed files with 111 additions and 10 deletions
+22 -4
View File
@@ -7,6 +7,11 @@ export const WASH_STEPS = {
COMPLETED: 5,
};
const normalizeLaneId = (value) => {
const normalized = parseInt(String(value ?? ""), 10);
return Number.isNaN(normalized) || normalized <= 0 ? null : normalized;
};
export function useWashFlowState(options) {
const {
currentStep,
@@ -35,6 +40,17 @@ export function useWashFlowState(options) {
activeTasks.value.length > 0 ? steps.TASKS : steps.WASH_IN_PROGRESS
);
const getSelectedLaneId = () => normalizeLaneId(radioLaneOption.value);
const getSelectedLane = () => {
const selectedLaneId = getSelectedLaneId();
if (selectedLaneId === null) {
return null;
}
return nearestDepartment.value?.lanes.find((lane) => normalizeLaneId(lane.id) === selectedLaneId) || null;
};
const clickableSteps = {
[steps.VEHICLE]: () => {
if (washInProgress.value) {
@@ -106,7 +122,7 @@ export function useWashFlowState(options) {
}
if (currentStep.value === steps.SELECT_LANE) {
const selectedLane = nearestDepartment.value?.lanes.find((lane) => lane.id === radioLaneOption.value);
const selectedLane = getSelectedLane();
if (!selectedLane || !isLaneAvailable(selectedLane)) {
return true;
}
@@ -136,9 +152,10 @@ export function useWashFlowState(options) {
const handleConfirmNext = async () => {
if (currentStep.value === steps.QUESTIONS) {
editAnswers.value = false;
if (radioLaneOption.value && radioLaneOption.value !== "Any") {
const selectedLaneId = getSelectedLaneId();
if (selectedLaneId !== null) {
try {
await updateLaneAllowedServices(radioLaneOption.value);
await updateLaneAllowedServices(selectedLaneId);
} catch {
return;
}
@@ -153,8 +170,9 @@ export function useWashFlowState(options) {
return;
}
const selectedLaneId = getSelectedLaneId() ?? radioLaneOption.value;
await onStartWash(
radioLaneOption.value,
selectedLaneId,
licensePlateInput.value,
customerNumberInput.value,
targetStepForStart()
@@ -121,6 +121,7 @@ const {
answers,
completedTasks,
allowedServices,
session: selfServeSession,
error: selfServeDataError,
visibleQuestions,
activeTasks,
@@ -203,8 +204,33 @@ const isLaneSelfServeEnabled = (lane: any) =>
const isCurrentActiveWashLane = (lane: any) =>
washInProgress.value && normalizeLaneId(lane?.id) === normalizeLaneId(washLaneId.value);
const isCurrentAllowedSelfServeLane = (lane: any) =>
selfServeAllowed.value === true && normalizeLaneId(lane?.id) === normalizeLaneId(selfServeLane.value?.id);
const isAllowedSelfServeValue = (value: any) =>
value === true ||
value === 1 ||
value === "1" ||
String(value ?? "")
.trim()
.toLowerCase() === "true";
const isSelfServeSessionAllowed = () =>
isAllowedSelfServeValue(selfServeAllowed.value) || isAllowedSelfServeValue(selfServeSession.value?.allowed);
const getAllowedSelfServeLaneIds = () =>
new Set(
[
selfServeLane.value?.id,
selfServeSession.value?.lane_id,
selfServeSession.value?.lane?.id,
selfServeSession.value?.metadata?.lane_id,
]
.map(normalizeLaneId)
.filter((laneId): laneId is number => laneId !== null)
);
const isCurrentAllowedSelfServeLane = (lane: any) => {
const laneId = normalizeLaneId(lane?.id);
return laneId !== null && isSelfServeSessionAllowed() && getAllowedSelfServeLaneIds().has(laneId);
};
const isLaneAvailable = (lane: any) =>
(isCurrentActiveWashLane(lane) || isCurrentAllowedSelfServeLane(lane) || lane.status === "AVAILABLE") &&
@@ -219,7 +245,9 @@ const isMachineAvailable = (laneId: number | string | null) => {
);
}
const selectedLane = nearestDepartment.value?.lanes.find((entry: { id: number }) => entry.id === laneId);
const selectedLane = nearestDepartment.value?.lanes.find(
(entry: { id: number }) => normalizeLaneId(entry.id) === normalizeLaneId(laneId)
);
if (!selectedLane) {
return false;
}
+5 -3
View File
@@ -52,11 +52,13 @@ describe("MyWashStart.vue production recovery contracts", () => {
});
it("does not treat the customer's active occupied lane as unavailable", () => {
expect(source).toContain("session: selfServeSession,");
expect(source).toContain("const isCurrentActiveWashLane = (lane: any) =>");
expect(source).toContain("const isSelfServeSessionAllowed = () =>");
expect(source).toContain("const isCurrentAllowedSelfServeLane = (lane: any) =>");
expect(source).toContain(
"selfServeAllowed.value === true && normalizeLaneId(lane?.id) === normalizeLaneId(selfServeLane.value?.id)"
);
expect(source).toContain("isAllowedSelfServeValue(selfServeSession.value?.allowed)");
expect(source).toContain("selfServeSession.value?.lane_id");
expect(source).toContain("getAllowedSelfServeLaneIds().has(laneId)");
expect(source).toContain(
'(isCurrentActiveWashLane(lane) || isCurrentAllowedSelfServeLane(lane) || lane.status === "AVAILABLE")'
);
+25
View File
@@ -26,6 +26,7 @@ const mocks = vi.hoisted(() => {
const allowedServices = { value: ["MACHINE"] };
const selfServeAllowed = { value: false };
const selfServeLane = { value: { id: 7 } };
const selfServeSession = { value: null };
return {
nearestDepartment,
@@ -40,6 +41,7 @@ const mocks = vi.hoisted(() => {
allowedServices,
selfServeAllowed,
selfServeLane,
selfServeSession,
fetchDepartments: vi.fn(async () => guestDepartments.value),
startAutoRefresh: vi.fn(),
stopAutoRefresh: vi.fn(),
@@ -176,6 +178,7 @@ vi.mock("@/composables/useSelfServeLogic", () => ({
completedTasks: mocks.completedTasks,
allowedServices: mocks.allowedServices,
allowed: mocks.selfServeAllowed,
session: mocks.selfServeSession,
visibleQuestions: mocks.visibleQuestions,
activeTasks: mocks.activeTasks,
allVisibleQuestionsAnswered: { value: false },
@@ -376,6 +379,7 @@ describe("MyWashStart", () => {
mocks.allowedServices.value = ["MACHINE"];
mocks.selfServeAllowed.value = false;
mocks.selfServeLane.value = { id: 7 };
mocks.selfServeSession.value = null;
mocks.conditions.value = [];
mocks.rules.value = [];
mocks.sessionCustomerNumber.value = 12345679;
@@ -517,6 +521,27 @@ describe("MyWashStart", () => {
expect(wrapper.get('[data-testid="lane-7-available"]').text()).toBe("true");
});
it("uses the allowed session lane when the lane payload is not populated yet", async () => {
mocks.nearestDepartment.value = {
...mocks.nearestDepartment.value,
lanes: [{ id: 7, name: "7", status: "OCCUPIED", products: [2], machine_available: true }],
};
mocks.guestDepartments.value = [mocks.nearestDepartment.value];
mocks.selfServeLane.value = null;
mocks.selfServeAllowed.value = false;
mocks.selfServeSession.value = { lane_id: 7, allowed: true };
const wrapper = mountWithApp(MyWashStart, {
global: {
stubs: stubComponents,
},
});
await flushPromises();
expect(wrapper.get('[data-testid="lane-7-available"]').text()).toBe("true");
});
it("hides machine tasks while manual wash is selected", async () => {
mocks.activeTasks.value = [
{ id: 31, task: "Machine checklist", services: ["MACHINE"] },
@@ -79,6 +79,17 @@ describe("useWashFlowState production transitions", () => {
expect(flow.isNextButtonDisabled()).toBe(true);
});
it("keeps lane confirmation enabled when the selected lane id is a string", async () => {
const { state, flow } = createFlow({ radioLaneOption: ref("7") });
state.currentStep.value = WASH_STEPS.SELECT_LANE;
expect(flow.isNextButtonDisabled()).toBe(false);
await flow.handleConfirmNext();
expect(state.onStartWash).toHaveBeenCalledWith(7, "AB12345", "12345679", WASH_STEPS.WASH_IN_PROGRESS);
});
it("stays on questions when lane allowed-service refresh fails", async () => {
const { state, flow } = createFlow({
updateLaneAllowedServices: vi.fn(async () => {
+17
View File
@@ -111,4 +111,21 @@ describe("useWashFlowState", () => {
expect(state.onStartWash).toHaveBeenCalledWith(7, "AB12345", "12345", WASH_STEPS.TASKS);
});
it("matches string lane selections against numeric lane ids", async () => {
const { flow, state } = createState({
currentStep: ref(WASH_STEPS.QUESTIONS),
radioLaneOption: ref("7"),
});
await flow.handleConfirmNext();
expect(state.updateLaneAllowedServices).toHaveBeenCalledWith(7);
expect(state.currentStep.value).toBe(WASH_STEPS.SELECT_LANE);
expect(flow.isNextButtonDisabled()).toBe(false);
await flow.handleConfirmNext();
expect(state.onStartWash).toHaveBeenCalledWith(7, "AB12345", "12345", WASH_STEPS.WASH_IN_PROGRESS);
});
});