import { MorLocationTuple } from "./mor-locations.data"; import { MorGeoMappingError, normalizeName, resolveMorGeo, tryResolveMorGeo, } from "./mor-location.resolver"; /** * Rows copied verbatim out of the Ministry sheet (`EIMS_COUNTRY_REGION_VW`), chosen for the traps * the real data contains rather than for tidiness: * * - BABILE and KERSA each exist in two different zones with different LOCALITY_NOs — the reason a * global name lookup is unsafe and the hierarchy is mandatory. * - ILLUBABOR has BURE twice under the same zone with different LOCALITY_NOs (691 and 890, the * second with the Ministry's own trailing space) — a genuine ambiguity that must never be * silently resolved to the first row. * - "Wal-Mera" and "Akaki woreda" carry the sheet's mixed casing and punctuation. */ const FIXTURE: MorLocationTuple[] = [ [70, "Ethiopia", 6, "SOMALI", 31, "FAAFAN ZONE", 190, "JIJIGA"], [70, "Ethiopia", 6, "SOMALI", 31, "FAAFAN ZONE", 194, "BABILE"], [70, "Ethiopia", 6, "SOMALI", 30, "SITI ZONE", 197, "DENBEL"], [70, "Ethiopia", 2, "OROMIA", 8, "MISRAK HARARGE", 495, "BABILE"], [70, "Ethiopia", 2, "OROMIA", 8, "MISRAK HARARGE", 482, "KERSA"], [70, "Ethiopia", 2, "OROMIA", 72, "JIMMA ZONE", 503, "KERSA"], [70, "Ethiopia", 2, "OROMIA", 73, "ILLUBABOR", 691, "BURE"], [70, "Ethiopia", 2, "OROMIA", 73, "ILLUBABOR", 890, "BURE "], [70, "Ethiopia", 2, "OROMIA", 86, "FINFINE VIC SPEC", 976, "Wal-Mera"], [70, "Ethiopia", 2, "OROMIA", 86, "FINFINE VIC SPEC", 909, "Akaki woreda"], [70, "Ethiopia", 13, "ADDIS ABABA", 78, "BOLE", 1100, "WOREDA 1"], [253, "Djibouti", 1, "DJIBOUTI", 1, "DJIBOUTI VILLE", 1, "BALBALA"], ]; const JIJIGA = { country: "Ethiopia", region: "SOMALI", zone: "FAAFAN ZONE", woreda: "JIJIGA", }; describe("normalizeName", () => { it("collapses whitespace, trims, and compares case-insensitively", () => { expect(normalizeName(" FAAFAN ZONE ")).toBe("FAAFAN ZONE"); expect(normalizeName("faafan zone")).toBe("FAAFAN ZONE"); expect(normalizeName(" FAAFAN ZONE ")).toBe(normalizeName("faafan zone")); }); it("normalizes harmless punctuation and hyphen/space differences", () => { expect(normalizeName("Wal-Mera")).toBe("WAL MERA"); expect(normalizeName("Wal Mera")).toBe("WAL MERA"); expect(normalizeName("ZONE 1 (AYSSAITA)")).toBe("ZONE 1 AYSSAITA"); expect(normalizeName("Ber'ano")).toBe("BERANO"); expect(normalizeName("KEAHORE/HADAT/")).toBe("KEAHORE HADAT"); }); it("keeps digits, which several MoR locality names depend on", () => { expect(normalizeName(" woreda 10 ")).toBe("WOREDA 10"); expect(normalizeName("WOREDA 1")).not.toBe(normalizeName("WOREDA 10")); }); }); describe("resolveMorGeo", () => { it("resolves the exact MoR spelling to the Ministry's own codes", () => { expect(resolveMorGeo(JIJIGA, FIXTURE)).toEqual({ Country: "70", Region: "6", City: "31", Wereda: "190", }); }); it("resolves the EDR/e-Trade spellings through the alias layer", () => { expect( resolveMorGeo( { country: "Ethiopia", region: "Somali", zone: "Fafen", woreda: "Jigjiga", }, FIXTURE, ), ).toEqual({ Country: "70", Region: "6", City: "31", Wereda: "190" }); }); it("is case-insensitive", () => { expect( resolveMorGeo( { country: "ethiopia", region: "somali", zone: "faafan zone", woreda: "jijiga", }, FIXTURE, ), ).toEqual({ Country: "70", Region: "6", City: "31", Wereda: "190" }); }); it("ignores leading, trailing and repeated whitespace on every level", () => { expect( resolveMorGeo( { country: " Ethiopia ", region: " SOMALI ", zone: " FAAFAN ZONE ", woreda: "\tJIJIGA ", }, FIXTURE, ), ).toEqual({ Country: "70", Region: "6", City: "31", Wereda: "190" }); }); it("treats a hyphen as a space, in either direction", () => { const expected = { Country: "70", Region: "2", City: "86", Wereda: "976" }; const base = { country: "Ethiopia", region: "Oromia", zone: "Finfine Vic Spec", }; expect(resolveMorGeo({ ...base, woreda: "Wal-Mera" }, FIXTURE)).toEqual(expected); expect(resolveMorGeo({ ...base, woreda: "wal mera" }, FIXTURE)).toEqual(expected); }); it("matches a zone whose MoR label carries the ' ZONE' suffix EDR does not store", () => { expect(resolveMorGeo({ ...JIJIGA, zone: "Faafan" }, FIXTURE).City).toBe("31"); expect( resolveMorGeo( { country: "Ethiopia", region: "Somali", zone: "Siti", woreda: "Denbel", }, FIXTURE, ), ).toEqual({ Country: "70", Region: "6", City: "30", Wereda: "197" }); }); describe("a locality name that exists in more than one zone", () => { it("picks BABILE by its full hierarchy, never by name alone", () => { expect(resolveMorGeo({ ...JIJIGA, woreda: "BABILE" }, FIXTURE).Wereda).toBe("194"); expect( resolveMorGeo( { country: "Ethiopia", region: "OROMIA", zone: "MISRAK HARARGE", woreda: "BABILE", }, FIXTURE, ).Wereda, ).toBe("495"); }); it("picks KERSA by its full hierarchy", () => { const oromia = { country: "Ethiopia", region: "OROMIA" }; expect( resolveMorGeo({ ...oromia, zone: "MISRAK HARARGE", woreda: "KERSA" }, FIXTURE).Wereda, ).toBe("482"); expect( resolveMorGeo({ ...oromia, zone: "JIMMA ZONE", woreda: "KERSA" }, FIXTURE).Wereda, ).toBe("503"); }); it("does not let a locality leak across regions", () => { // DENBEL exists under SOMALI/SITI ZONE only — asking for it under OROMIA must fail, not // fall back to the nationwide match the old flat maps would have found. expect(() => resolveMorGeo( { country: "Ethiopia", region: "OROMIA", zone: "MISRAK HARARGE", woreda: "DENBEL", }, FIXTURE, ), ).toThrow(/no MoR LOCALITY_DESC match/); }); }); describe("failures happen locally, before anything is filed", () => { const cases: Array<[string, Record, RegExp]> = [ ["unknown country", { ...JIJIGA, country: "Wakanda" }, /no MoR COUNTRY_NAME match/], ["unknown region", { ...JIJIGA, region: "Atlantis" }, /no MoR PARISH_NAME match/], ["unknown zone", { ...JIJIGA, zone: "Nowhere Zone" }, /no MoR CITY_NAME match/], ["unknown woreda", { ...JIJIGA, woreda: "Example" }, /no MoR LOCALITY_DESC match/], ]; it.each(cases)("%s fails with an actionable validation error", (_label, input, pattern) => { expect(() => resolveMorGeo(input, FIXTURE)).toThrow(MorGeoMappingError); expect(() => resolveMorGeo(input, FIXTURE)).toThrow(pattern); }); it("names the offending address in the message so the company record can be corrected", () => { expect(() => resolveMorGeo({ ...JIJIGA, woreda: "Example" }, FIXTURE)).toThrow( /country="Ethiopia", region="SOMALI", zone="FAAFAN ZONE", woreda="Example"/, ); }); it("refuses an ambiguous locality instead of taking the first row", () => { const input = { country: "Ethiopia", region: "OROMIA", zone: "ILLUBABOR", woreda: "BURE", }; expect(() => resolveMorGeo(input, FIXTURE)).toThrow(MorGeoMappingError); expect(() => resolveMorGeo(input, FIXTURE)).toThrow(/ambiguous/); // Both colliding codes are named, and neither is silently selected. expect(() => resolveMorGeo(input, FIXTURE)).toThrow(/691, 890/); expect(tryResolveMorGeo(input, FIXTURE)).toBeNull(); }); it("fails loudly when the MoR master has not been generated yet", () => { expect(() => resolveMorGeo(JIJIGA, [])).toThrow(/MoR location master is empty/); }); }); it("reproduces MoR's numeric values unchanged, as strings", () => { const codes = resolveMorGeo(JIJIGA, FIXTURE); expect(codes).toEqual({ Country: "70", Region: "6", City: "31", Wereda: "190", }); for (const value of Object.values(codes)) { expect(typeof value).toBe("string"); expect(value).toMatch(/^[0-9]+$/); } // The source row is the only origin of every code — no renumbering, no derivation. const [countryNo, , parishNo, , cityNo, , localityNo] = FIXTURE[0]; expect(codes).toEqual({ Country: String(countryNo), Region: String(parishNo), City: String(cityNo), Wereda: String(localityNo), }); }); it("never emits an Open Admin Data ETxx identifier", () => { for (const value of Object.values(resolveMorGeo(JIJIGA, FIXTURE))) { expect(value).not.toMatch(/^ET/i); } }); it("treats a blank country as domestic, matching the column default", () => { expect(resolveMorGeo({ ...JIJIGA, country: "" }, FIXTURE).Country).toBe("70"); expect(resolveMorGeo({ ...JIJIGA, country: null }, FIXTURE).Country).toBe("70"); }); it("resolves a named foreign country rather than defaulting it to Ethiopia", () => { expect( resolveMorGeo( { country: "Djibouti", region: "DJIBOUTI", zone: "DJIBOUTI VILLE", woreda: "BALBALA", }, FIXTURE, ), ).toEqual({ Country: "253", Region: "1", City: "1", Wereda: "1" }); }); it("accepts a company record that already holds a MoR code, but only a real one", () => { expect(resolveMorGeo({ ...JIJIGA, region: "6" }, FIXTURE).Region).toBe("6"); expect(() => resolveMorGeo({ ...JIJIGA, region: "999" }, FIXTURE)).toThrow( /no MoR PARISH_NAME match/, ); }); });