Fix POS step 1 customer lookup loop
This commit is contained in:
@@ -217,6 +217,44 @@ const getVehicleReferenceValue = () => {
|
||||
return String(vehicleObject.value?.reference ?? "").trim();
|
||||
};
|
||||
|
||||
const getVehicleStateKey = (vehicle) => {
|
||||
if (!vehicle) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return [
|
||||
vehicle?.id ?? "",
|
||||
normalizePlateValue(vehicle?.reg),
|
||||
normalizeCustomerNumber(vehicle?.customer_id) ?? "",
|
||||
vehicle?.type ?? "",
|
||||
vehicle?.booking_id ?? "",
|
||||
String(vehicle?.reference ?? "").trim(),
|
||||
].join("|");
|
||||
};
|
||||
|
||||
const getBookingStateKey = (booking) => {
|
||||
if (!booking) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return [
|
||||
booking?.id ?? "",
|
||||
getBookingReg1Value(booking),
|
||||
getBookingReg2Value(booking),
|
||||
getBookingCustomerNumber(booking) ?? "",
|
||||
String(booking?.reference_number ?? booking?.reference ?? "").trim(),
|
||||
String(booking?.po ?? "").trim(),
|
||||
].join("|");
|
||||
};
|
||||
|
||||
const getBookingMatchesStateKey = (matches) => {
|
||||
if (!Array.isArray(matches) || matches.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return matches.map((booking) => getBookingStateKey(booking)).join("||");
|
||||
};
|
||||
|
||||
const setVehicleObject = (emittedVehicleObject, options = {}) => {
|
||||
const normalizedOptions = {
|
||||
preserveManualReference: true,
|
||||
@@ -224,17 +262,26 @@ const setVehicleObject = (emittedVehicleObject, options = {}) => {
|
||||
...options,
|
||||
};
|
||||
|
||||
vehicleObject.value = emittedVehicleObject;
|
||||
|
||||
const emittedVehiclePlate = normalizePlateValue(vehicleObject.value?.reg || reg_1.value);
|
||||
const emittedVehiclePlate = normalizePlateValue(emittedVehicleObject?.reg || reg_1.value);
|
||||
if (
|
||||
emittedVehiclePlate &&
|
||||
skippedDesktopBookingVehiclePlate.value === emittedVehiclePlate &&
|
||||
!isBookingMarkedVehicle(vehicleObject.value)
|
||||
!isBookingMarkedVehicle(emittedVehicleObject)
|
||||
) {
|
||||
skippedDesktopBookingVehiclePlate.value = "";
|
||||
}
|
||||
|
||||
const currentVehicleKey = getVehicleStateKey(vehicleObject.value);
|
||||
const nextVehicleKey = getVehicleStateKey(emittedVehicleObject);
|
||||
if (currentVehicleKey === nextVehicleKey) {
|
||||
if (normalizedOptions.nextSelectionSource) {
|
||||
setSelectionSource(normalizedOptions.nextSelectionSource);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
vehicleObject.value = emittedVehicleObject;
|
||||
|
||||
const matchedVehicleCustomerNumber = normalizeCustomerNumber(vehicleObject.value?.customer_id);
|
||||
const selectedCustomerNumber = normalizeCustomerNumber(customer_id.value);
|
||||
|
||||
@@ -274,6 +321,10 @@ const setBookingObject = (emittedBookingObject, options = {}) => {
|
||||
...options,
|
||||
};
|
||||
|
||||
if (getBookingStateKey(bookingObject.value) === getBookingStateKey(emittedBookingObject)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bookingObject.value = emittedBookingObject;
|
||||
|
||||
if (emittedBookingObject && emittedBookingObject.reference_number) {
|
||||
@@ -284,7 +335,12 @@ const setBookingObject = (emittedBookingObject, options = {}) => {
|
||||
};
|
||||
|
||||
const setBookingMatches = (emittedBookingMatches) => {
|
||||
bookingMatches.value = Array.isArray(emittedBookingMatches) ? emittedBookingMatches : [];
|
||||
const nextBookingMatches = Array.isArray(emittedBookingMatches) ? emittedBookingMatches : [];
|
||||
if (getBookingMatchesStateKey(bookingMatches.value) === getBookingMatchesStateKey(nextBookingMatches)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bookingMatches.value = nextBookingMatches;
|
||||
};
|
||||
|
||||
const mergeBookingMatchDetails = (booking) => {
|
||||
|
||||
@@ -416,6 +416,21 @@ const shouldPreserveCustomerSelection = (plateValue) => {
|
||||
};
|
||||
|
||||
let pendingVehicleCustomerLookup = null;
|
||||
const lastAutoSyncedVehicleKey = ref("");
|
||||
|
||||
const getVehicleAutoSyncKey = (vehicle, plateOverride = null) => {
|
||||
if (!vehicle) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return [
|
||||
resolveBookingPlate(vehicle, plateOverride),
|
||||
vehicle?.id ?? "",
|
||||
normalizeCustomerNumber(vehicle?.customer_id) ?? "",
|
||||
normalizeCustomerNumber(customer_id.value) ?? "",
|
||||
].join("|");
|
||||
};
|
||||
|
||||
const syncMatchedVehicleSelection = (vehicle, options = {}) => {
|
||||
const normalizedOptions = {
|
||||
plateOverride: null,
|
||||
@@ -590,6 +605,7 @@ watch(reg_1, (newValue) => {
|
||||
// Define the search ID for this input change
|
||||
const search_id = register_new_search();
|
||||
console.log("reg_1 changed:", newValue);
|
||||
lastAutoSyncedVehicleKey.value = "";
|
||||
const shouldKeepCustomerSelection = shouldPreserveCustomerSelection(newValue);
|
||||
selectedDropdownItem.value = -1;
|
||||
// Check if the new value is empty, if so, clear the vehicles_matching array
|
||||
@@ -847,11 +863,17 @@ watch(vehicles_matching, (newValue) => {
|
||||
const currentValue = reg_1.value;
|
||||
const vehicle = newValue.find((vehicle) => vehicle.reg === currentValue);
|
||||
if (vehicle) {
|
||||
syncMatchedVehicleSelection(vehicle);
|
||||
const nextAutoSyncKey = getVehicleAutoSyncKey(vehicle, currentValue);
|
||||
if (lastAutoSyncedVehicleKey.value !== nextAutoSyncKey) {
|
||||
lastAutoSyncedVehicleKey.value = nextAutoSyncKey;
|
||||
syncMatchedVehicleSelection(vehicle);
|
||||
}
|
||||
} else if (getBookingMatchesForSelection(null, currentValue).length > 0) {
|
||||
lastAutoSyncedVehicleKey.value = "";
|
||||
clearCustomerConflict();
|
||||
emitVehicleObject(null, currentValue);
|
||||
} else {
|
||||
lastAutoSyncedVehicleKey.value = "";
|
||||
clearCustomerConflict();
|
||||
}
|
||||
// Check if the selectedDropdownItem index is valid
|
||||
|
||||
@@ -1188,6 +1188,7 @@ export const selectScan = (scan) => {
|
||||
};
|
||||
|
||||
const cached_customer_names = [];
|
||||
const activeCustomerSelectionRequests = new Map();
|
||||
|
||||
/** Get the customer's name */
|
||||
export const getCustomerName = async (customerNumber) => {
|
||||
@@ -1216,23 +1217,62 @@ export const getCustomerName = async (customerNumber) => {
|
||||
|
||||
/** Search, then select customer by customer number */
|
||||
export const searchAndSelectCustomer = async (customerNumber, options = {}) => {
|
||||
const normalizedOptions = {
|
||||
forceRefresh: false,
|
||||
...options,
|
||||
};
|
||||
const normalizedCustomerNumber = resolveCustomerNumber(customerNumber);
|
||||
if (!normalizedCustomerNumber) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedCustomerNumber = getSelectedCustomerNumber();
|
||||
const hasSelectedCustomerData =
|
||||
customer_data.value &&
|
||||
typeof customer_data.value === "object" &&
|
||||
!Array.isArray(customer_data.value) &&
|
||||
Object.keys(customer_data.value).length > 0;
|
||||
|
||||
if (
|
||||
!normalizedOptions.forceRefresh &&
|
||||
selectedCustomerNumber === normalizedCustomerNumber &&
|
||||
hasSelectedCustomerData
|
||||
) {
|
||||
return customer_data.value;
|
||||
}
|
||||
|
||||
if (!normalizedOptions.forceRefresh && activeCustomerSelectionRequests.has(normalizedCustomerNumber)) {
|
||||
return await activeCustomerSelectionRequests.get(normalizedCustomerNumber);
|
||||
}
|
||||
|
||||
// Get the customer data
|
||||
return await authenticatedRequest(`/users/customer?customer_number=${customerNumber}`, "GET")
|
||||
const request = authenticatedRequest(`/users/customer?customer_number=${normalizedCustomerNumber}`, "GET")
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
const responseData = response?.data?.data ?? {};
|
||||
const rawEconomicCustomer = responseData.economic_customer ?? null;
|
||||
const normalizedCustomer = normalizeCustomerRecord(rawEconomicCustomer, {
|
||||
...responseData,
|
||||
customerNumber: resolveCustomerNumber(rawEconomicCustomer) ?? resolveCustomerNumber(customerNumber),
|
||||
customerNumber: resolveCustomerNumber(rawEconomicCustomer) ?? normalizedCustomerNumber,
|
||||
});
|
||||
selectCustomer(normalizedCustomer, options);
|
||||
selectCustomer(normalizedCustomer, normalizedOptions);
|
||||
return normalizedCustomer;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
return null;
|
||||
})
|
||||
.finally(() => {
|
||||
if (activeCustomerSelectionRequests.get(normalizedCustomerNumber) === request) {
|
||||
activeCustomerSelectionRequests.delete(normalizedCustomerNumber);
|
||||
}
|
||||
});
|
||||
|
||||
if (!normalizedOptions.forceRefresh) {
|
||||
activeCustomerSelectionRequests.set(normalizedCustomerNumber, request);
|
||||
}
|
||||
|
||||
return await request;
|
||||
};
|
||||
|
||||
/** Get order items */
|
||||
|
||||
@@ -116,12 +116,17 @@ vi.mock("sweetalert2", () => ({
|
||||
}));
|
||||
|
||||
import axios from "axios";
|
||||
import { getAttributes } from "@/components/shop/CustomerAttributes.vue";
|
||||
import { getNotes } from "@/components/shop/CustomerNotes.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||
import {
|
||||
clearActivePosOrderContext,
|
||||
clearCustomerSelection,
|
||||
clearStoredPosOrderId,
|
||||
createOrder,
|
||||
completed_at,
|
||||
customer_data,
|
||||
customer_id,
|
||||
customer_name,
|
||||
department_id,
|
||||
@@ -137,6 +142,7 @@ import {
|
||||
reg_2,
|
||||
reg_3,
|
||||
restoreStoredPosOrderId,
|
||||
searchAndSelectCustomer,
|
||||
scan_data,
|
||||
scans,
|
||||
step,
|
||||
@@ -192,6 +198,77 @@ describe("POSDepartmentProcess.loadOrderItems", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("POSDepartmentProcess.searchAndSelectCustomer", () => {
|
||||
beforeEach(() => {
|
||||
clearCustomerSelection();
|
||||
authenticatedRequest.mockReset();
|
||||
getNotes.mockReset();
|
||||
getNotes.mockResolvedValue({ data: { data: [] } });
|
||||
getAttributes.mockReset();
|
||||
getAttributes.mockResolvedValue({ data: { success: true, data: [] } });
|
||||
});
|
||||
|
||||
const mockCustomerResponse = (customerNumber = 12345679, name = "Pleno Logistics") => ({
|
||||
data: {
|
||||
data: {
|
||||
customer_number: customerNumber,
|
||||
customer_name: name,
|
||||
economic_customer: {
|
||||
customerNumber,
|
||||
name,
|
||||
barred: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it("returns the already selected customer without a duplicate lookup", async () => {
|
||||
authenticatedRequest.mockResolvedValueOnce(mockCustomerResponse());
|
||||
|
||||
const selectedCustomer = await searchAndSelectCustomer(12345679);
|
||||
const cachedCustomer = await searchAndSelectCustomer(12345679);
|
||||
|
||||
expect(authenticatedRequest).toHaveBeenCalledTimes(1);
|
||||
expect(authenticatedRequest).toHaveBeenCalledWith("/users/customer?customer_number=12345679", "GET");
|
||||
expect(cachedCustomer).toBe(customer_data.value);
|
||||
expect(cachedCustomer).toEqual(selectedCustomer);
|
||||
expect(customer_id.value).toBe(12345679);
|
||||
expect(customer_name.value).toBe("Pleno Logistics");
|
||||
});
|
||||
|
||||
it("shares concurrent lookups for the same customer number", async () => {
|
||||
let resolveRequest;
|
||||
authenticatedRequest.mockReturnValueOnce(
|
||||
new Promise((resolve) => {
|
||||
resolveRequest = resolve;
|
||||
})
|
||||
);
|
||||
|
||||
const firstLookup = searchAndSelectCustomer(12345679);
|
||||
const secondLookup = searchAndSelectCustomer(12345679);
|
||||
resolveRequest(mockCustomerResponse());
|
||||
|
||||
const [firstCustomer, secondCustomer] = await Promise.all([firstLookup, secondLookup]);
|
||||
|
||||
expect(authenticatedRequest).toHaveBeenCalledTimes(1);
|
||||
expect(firstCustomer).toEqual(secondCustomer);
|
||||
expect(customer_id.value).toBe(12345679);
|
||||
});
|
||||
|
||||
it("performs a new lookup when forceRefresh is requested", async () => {
|
||||
authenticatedRequest
|
||||
.mockResolvedValueOnce(mockCustomerResponse(12345679, "Pleno Logistics"))
|
||||
.mockResolvedValueOnce(mockCustomerResponse(12345679, "Pleno Logistics Updated"));
|
||||
|
||||
await searchAndSelectCustomer(12345679);
|
||||
const refreshedCustomer = await searchAndSelectCustomer(12345679, { forceRefresh: true });
|
||||
|
||||
expect(authenticatedRequest).toHaveBeenCalledTimes(2);
|
||||
expect(refreshedCustomer.name).toBe("Pleno Logistics Updated");
|
||||
expect(customer_name.value).toBe("Pleno Logistics Updated");
|
||||
});
|
||||
});
|
||||
|
||||
describe("POSDepartmentProcess.clearActivePosOrderContext", () => {
|
||||
it("clears active order, customer, metadata and stored mobile order id", () => {
|
||||
localStorage.setItem("pos_order_id", "51211");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @vitest-environment jsdom
|
||||
import { nextTick, ref } from "vue";
|
||||
import { flushPromises } from "@vue/test-utils";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { mountWithApp } from "./helpers/mountWithApp.js";
|
||||
|
||||
@@ -223,9 +224,17 @@ const LicensePlateReg1InputStub = {
|
||||
emitLinkedVehicle() {
|
||||
this.$emit("update:vehicleObject", { ...linkedVehicle });
|
||||
},
|
||||
emitLinkedVehicleTwice() {
|
||||
this.$emit("update:vehicleObject", { ...linkedVehicle });
|
||||
this.$emit("update:vehicleObject", { ...linkedVehicle });
|
||||
},
|
||||
emitBookedVehicle() {
|
||||
this.$emit("update:vehicleObject", { ...bookedVehicle });
|
||||
},
|
||||
emitLinkedBookingMatchesTwice() {
|
||||
this.$emit("update:bookingMatches", [{ ...bookingWithItems }]);
|
||||
this.$emit("update:bookingMatches", [{ ...bookingWithItems }]);
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
@@ -243,6 +252,13 @@ const LicensePlateReg1InputStub = {
|
||||
>
|
||||
Emit linked vehicle
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="emit-linked-vehicle-twice"
|
||||
@click="emitLinkedVehicleTwice"
|
||||
>
|
||||
Emit linked vehicle twice
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="emit-booked-vehicle"
|
||||
@@ -250,6 +266,13 @@ const LicensePlateReg1InputStub = {
|
||||
>
|
||||
Emit booked vehicle
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="emit-linked-booking-matches-twice"
|
||||
@click="emitLinkedBookingMatchesTwice"
|
||||
>
|
||||
Emit linked booking matches twice
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
@@ -422,6 +445,27 @@ describe("SelectVehicleFormPOS", () => {
|
||||
expect(wrapper.findAll('[data-testid="customer-search-card-payment-stub"]')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("ignores repeated identical linked vehicle emissions", async () => {
|
||||
const wrapper = mountForm();
|
||||
|
||||
await wrapper.get('[data-testid="emit-linked-vehicle-twice"]').trigger("click");
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.findAll('[data-testid="customer-search-field-pos-stub"]')).toHaveLength(0);
|
||||
expect(posState.searchAndSelectCustomer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not re-apply identical booking matches", async () => {
|
||||
const wrapper = mountForm();
|
||||
|
||||
await wrapper.get('[data-testid="emit-linked-booking-matches-twice"]').trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
expect(posState.setSelectedOrderBookingSelection).toHaveBeenCalledTimes(1);
|
||||
expect(posState.searchAndSelectCustomer).toHaveBeenCalledTimes(1);
|
||||
expect(posState.searchAndSelectCustomer).toHaveBeenCalledWith(12345679);
|
||||
});
|
||||
|
||||
it("hides a booked desktop vehicle summary after continuing without booking", async () => {
|
||||
const wrapper = mountForm();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user