import { afterAll, beforeEach, describe, expect, it } from "vitest"; import db from "../../app/db.server"; import { findEligibleLocationsForDelivery, geocodeAddress, meetsDeliveryDensity } from "../../app/services/zones.server"; const shopDomain = "zones-integration-test.myshopify.com"; async function cleanup() { await db.booking.deleteMany({ where: { shopDomain } }); await db.zone.deleteMany({ where: { shopDomain } }); await db.location.deleteMany({ where: { shopDomain } }); await db.geocodeCache.deleteMany({ where: { normalizedKey: { contains: "123 test" } } }); } describe("zones.server integration", () => { beforeEach(cleanup); afterAll(async () => { await cleanup(); await db.$disconnect(); }); it("geocodeAddress caches a result and reuses it without a real API key on the second call", async () => { // No GOOGLE_MAPS_API_KEY is set in this test environment, so the first // call would normally return null — pre-seed the cache directly to // prove the cache-read path works without needing live Maps access. await db.geocodeCache.upsert({ where: { normalizedKey: "123 test street" }, create: { normalizedKey: "123 test street", lat: 43.65, lng: -79.38 }, update: {}, }); const result = await geocodeAddress("123 Test Street"); expect(result).toEqual({ lat: 43.65, lng: -79.38 }); }); it("returns null when there's no cache entry and no API key configured", async () => { delete process.env.GOOGLE_MAPS_API_KEY; const result = await geocodeAddress("456 Nowhere Cached Ave"); expect(result).toBeNull(); }); it("findEligibleLocationsForDelivery matches a postal-zone location by postal code alone (no geocoding needed)", async () => { const location = await db.location.create({ data: { shopDomain, name: "Postal Loc", address: "", timezone: "America/Toronto" }, }); const zone = await db.zone.create({ data: { shopDomain, locationId: location.id, name: "Downtown", type: "postal", postalCodes: ["M5V 3A8"], }, }); const matches = await findEligibleLocationsForDelivery(shopDomain, { postalCode: "M5V3A8" }); expect(matches).toHaveLength(1); expect(matches[0]).toMatchObject({ locationId: location.id, zoneId: zone.id, distanceKm: null }); }); it("excludes an inactive location's zones", async () => { const location = await db.location.create({ data: { shopDomain, name: "Inactive Loc", address: "", timezone: "America/Toronto", active: false }, }); await db.zone.create({ data: { shopDomain, locationId: location.id, name: "Zone", type: "postal", postalCodes: ["M5V 3A8"] }, }); const matches = await findEligibleLocationsForDelivery(shopDomain, { postalCode: "M5V3A8" }); expect(matches).toHaveLength(0); }); it("ranks radius-zone matches nearest-first using cached geocode coordinates", async () => { await db.geocodeCache.upsert({ where: { normalizedKey: "123 test customer address" }, create: { normalizedKey: "123 test customer address", lat: 43.6532, lng: -79.3832 }, // Toronto update: {}, }); const near = await db.location.create({ data: { shopDomain, name: "Near", address: "", timezone: "America/Toronto", lat: 43.65, lng: -79.4 }, }); const far = await db.location.create({ data: { shopDomain, name: "Far", address: "", timezone: "America/Toronto", lat: 45.4215, lng: -75.6972 }, // Ottawa }); await db.zone.create({ data: { shopDomain, locationId: near.id, name: "Zone", type: "radius", radiusKm: 500 }, }); await db.zone.create({ data: { shopDomain, locationId: far.id, name: "Zone", type: "radius", radiusKm: 500 }, }); const matches = await findEligibleLocationsForDelivery(shopDomain, { address: "123 Test Customer Address" }); expect(matches).toHaveLength(2); expect(matches[0].locationId).toBe(near.id); expect(matches[1].locationId).toBe(far.id); }); it("meetsDeliveryDensity passes when minOrders is unset", async () => { const location = await db.location.create({ data: { shopDomain, name: "Loc", address: "", timezone: "America/Toronto" }, }); const zone = await db.zone.create({ data: { shopDomain, locationId: location.id, name: "Zone", type: "postal", postalCodes: [] }, }); expect(await meetsDeliveryDensity(shopDomain, zone)).toBe(true); }); it("meetsDeliveryDensity fails below threshold and passes once enough bookings exist", async () => { const location = await db.location.create({ data: { shopDomain, name: "Loc", address: "", timezone: "America/Toronto" }, }); const zone = await db.zone.create({ data: { shopDomain, locationId: location.id, name: "Sparse Zone", type: "postal", postalCodes: [], minOrders: 2 }, }); expect(await meetsDeliveryDensity(shopDomain, zone)).toBe(false); await db.booking.create({ data: { shopDomain, orderId: "gid://shopify/Order/zone-density-1", locationId: location.id, zoneId: zone.id, method: "LOCAL_DELIVERY", slotStart: new Date(), slotEnd: new Date(), }, }); expect(await meetsDeliveryDensity(shopDomain, zone)).toBe(false); // still only 1 of 2 await db.booking.create({ data: { shopDomain, orderId: "gid://shopify/Order/zone-density-2", locationId: location.id, zoneId: zone.id, method: "LOCAL_DELIVERY", slotStart: new Date(), slotEnd: new Date(), }, }); expect(await meetsDeliveryDensity(shopDomain, zone)).toBe(true); }); it("meetsDeliveryDensity does not count a cancelled booking toward the threshold", async () => { const location = await db.location.create({ data: { shopDomain, name: "Loc", address: "", timezone: "America/Toronto" }, }); const zone = await db.zone.create({ data: { shopDomain, locationId: location.id, name: "Zone", type: "postal", postalCodes: [], minOrders: 1 }, }); await db.booking.create({ data: { shopDomain, orderId: "gid://shopify/Order/zone-density-cancelled", locationId: location.id, zoneId: zone.id, method: "LOCAL_DELIVERY", slotStart: new Date(), slotEnd: new Date(), status: "cancelled", }, }); expect(await meetsDeliveryDensity(shopDomain, zone)).toBe(false); }); });