import { describe, it, expect } from "vitest"; import { distanceKm } from "./geo"; import { rankNearbyCustomers, DEFAULT_RADIUS_KM, type NearbyCandidate, } from "./visit-matching"; /** * Fixtures are anchored on Sibanye Driefontein (Carletonville) — the * same part of the West Rand the real customer base sits in, so the * latitude-dependent longitude scaling in `distanceKm` is exercised at * a realistic value rather than at the equator. */ const ORIGIN = [-26.4, 27.4] as const; /** ~1 km north of ORIGIN (1 degree of latitude ≈ 111.19 km). */ const ONE_KM_NORTH = { lat: -26.4 + 1 / 111.19, lon: 27.4 }; /** ~3 km north of ORIGIN — outside a 2 km radius. */ const THREE_KM_NORTH = { lat: -26.4 + 3 / 111.19, lon: 27.4 }; function candidate(over: Partial & { id: string }): NearbyCandidate { return { customer: `Customer ${over.id}`, coordinates: { lat: ORIGIN[0], lon: ORIGIN[1] }, rep: "", companyName: null, ...over, }; } describe("rankNearbyCustomers", () => { it("returns customers inside the radius and drops those outside it", () => { const result = rankNearbyCustomers({ origin: ORIGIN, radiusKm: 2, customers: [ candidate({ id: "near", coordinates: ONE_KM_NORTH }), candidate({ id: "far", coordinates: THREE_KM_NORTH }), ], }); expect(result.map((r) => r.id)).toEqual(["near"]); }); it("skips Null Island customers rather than placing them off West Africa", () => { // This is the state of all 616 customers before the pin tool / // capture-on-visit runs, so it is the common case, not an edge case. const result = rankNearbyCustomers({ origin: ORIGIN, radiusKm: 2, customers: [ candidate({ id: "unset", coordinates: { lat: 0, lon: 0 } }), candidate({ id: "pinned" }), ], }); expect(result.map((r) => r.id)).toEqual(["pinned"]); }); it("skips customers whose coordinates field is missing entirely", () => { const result = rankNearbyCustomers({ origin: ORIGIN, radiusKm: 2, customers: [ candidate({ id: "null-coords", coordinates: null }), candidate({ id: "undef-coords", coordinates: undefined }), candidate({ id: "pinned" }), ], }); expect(result.map((r) => r.id)).toEqual(["pinned"]); }); it("puts the signed-in rep's own customers first, then sorts by distance", () => { // Inside a 2 km radius everyone is on the same mine property, so // distance is mostly noise — the rep came to see *their* accounts. const result = rankNearbyCustomers({ origin: ORIGIN, radiusKm: 2, repId: "rep-tiaan", customers: [ candidate({ id: "other-closest", rep: "rep-chris" }), candidate({ id: "mine-far", rep: "rep-tiaan", coordinates: ONE_KM_NORTH }), candidate({ id: "mine-close", rep: "rep-tiaan" }), ], }); expect(result.map((r) => r.id)).toEqual([ "mine-close", "mine-far", "other-closest", ]); }); it("marks which rows belong to the signed-in rep", () => { const result = rankNearbyCustomers({ origin: ORIGIN, radiusKm: 2, repId: "rep-tiaan", customers: [ candidate({ id: "mine", rep: "rep-tiaan" }), candidate({ id: "theirs", rep: "rep-chris" }), ], }); expect(result.find((r) => r.id === "mine")?.isMine).toBe(true); expect(result.find((r) => r.id === "theirs")?.isMine).toBe(false); }); it("treats every row as not-mine when no rep is signed in", () => { const result = rankNearbyCustomers({ origin: ORIGIN, radiusKm: 2, customers: [candidate({ id: "a", rep: "rep-tiaan" })], }); expect(result[0].isMine).toBe(false); }); it("reports distance in metres-precision kilometres", () => { const [row] = rankNearbyCustomers({ origin: ORIGIN, radiusKm: 2, customers: [candidate({ id: "a", coordinates: ONE_KM_NORTH })], }); expect(row.distanceKm).toBeCloseTo(1, 2); }); it("carries the company name through for display", () => { const [row] = rankNearbyCustomers({ origin: ORIGIN, radiusKm: 2, customers: [ candidate({ id: "a", customer: "DANIE GREYLING", companyName: "Sibanye - Driefontein" }), ], }); expect(row.name).toBe("DANIE GREYLING"); expect(row.companyName).toBe("Sibanye - Driefontein"); }); it("includes a customer sitting exactly on the radius boundary", () => { // A rep parked exactly `radiusKm` from a pin should still see it — // excluding the boundary makes the radius setting feel arbitrary. // The radius is taken from the measured distance so this asserts the // exact `<=` boundary rather than an approximated one. const exactDistance = distanceKm(ORIGIN, [ ONE_KM_NORTH.lat, ONE_KM_NORTH.lon, ]); const result = rankNearbyCustomers({ origin: ORIGIN, radiusKm: exactDistance, customers: [candidate({ id: "boundary", coordinates: ONE_KM_NORTH })], }); expect(result.map((r) => r.id)).toEqual(["boundary"]); }); it("excludes a customer just outside the radius", () => { const exactDistance = distanceKm(ORIGIN, [ ONE_KM_NORTH.lat, ONE_KM_NORTH.lon, ]); const result = rankNearbyCustomers({ origin: ORIGIN, radiusKm: exactDistance - 0.001, customers: [candidate({ id: "boundary", coordinates: ONE_KM_NORTH })], }); expect(result).toEqual([]); }); it("falls back to the default radius when given a non-positive one", () => { // A misconfigured settings row must not silently match every // customer in the country (radius 0 → nothing, negative → nothing). const result = rankNearbyCustomers({ origin: ORIGIN, radiusKm: 0, customers: [candidate({ id: "near", coordinates: ONE_KM_NORTH })], }); expect(DEFAULT_RADIUS_KM).toBe(2); expect(result.map((r) => r.id)).toEqual(["near"]); }); it("returns an empty list rather than throwing on no candidates", () => { expect( rankNearbyCustomers({ origin: ORIGIN, radiusKm: 2, customers: [] }) ).toEqual([]); }); });