import type { Method } from "@prisma/client"; import db from "../db.server"; import { excludeLocationsWithoutStock, type AdminGraphQLClient } from "./zones.server"; // Multi-pin pickup selection (study §3.4 "Store pickup locations are shown // visually on Google Maps"). A shop can have several pickup points; the // shopper should be able to choose one before the date picker narrows to // that location's calendar. This resolves the *set* of eligible pickup // locations; resolveAvailabilityRequest then still returns one location's // worth of dates (the chosen one, or the first eligible). export interface PickupLocationDto { id: string; name: string; address: string; lat: number | null; lng: number | null; } export interface PickupLocationFilterInput { /** All active locations for the shop (already narrowed by any ProductRule allowedLocationIds). */ candidates: Array<{ id: string }>; /** Location ids that have at least one PICKUP SlotTemplate — a location with no pickup slots can't be picked up from. */ pickupTemplateLocationIds: Set; /** Location ids that stock the cart (identity set when the inventory check is off). */ stockedLocationIds: Set; } /** * Pure intersection: a pickup location must be an active candidate, offer * PICKUP slots, and stock the cart. Order is preserved from `candidates` * (which the caller sorts by createdAt). */ export function filterPickupLocationIds(input: PickupLocationFilterInput): string[] { const { candidates, pickupTemplateLocationIds, stockedLocationIds } = input; return candidates .map((c) => c.id) .filter((id) => pickupTemplateLocationIds.has(id) && stockedLocationIds.has(id)); } /** * The eligible pickup locations for a shop, oldest-first. Applies the same * ProductRule location restriction and inventory exclusion as the single- * location path in resolveAvailabilityRequest, so the map/list the shopper * sees never offers a location the order can't actually use. */ export async function resolvePickupLocations( shopDomain: string, opts: { allowedLocationIds?: string[] | null; admin?: AdminGraphQLClient; productVariantGids?: string[]; } = {}, ): Promise { const locationWhere = opts.allowedLocationIds != null ? { id: { in: opts.allowedLocationIds } } : {}; const [candidates, pickupTemplates] = await Promise.all([ db.location.findMany({ where: { shopDomain, active: true, ...locationWhere }, orderBy: { createdAt: "asc" }, select: { id: true, name: true, address: true, lat: true, lng: true }, }), db.slotTemplate.findMany({ where: { shopDomain, method: "PICKUP" as Method }, select: { locationId: true }, }), ]); const pickupTemplateLocationIds = new Set(pickupTemplates.map((t) => t.locationId)); const stockCheckEnabled = Boolean(opts.admin) && (opts.productVariantGids?.length ?? 0) > 0; const stockedLocationIds = stockCheckEnabled ? await excludeLocationsWithoutStock( opts.admin!, candidates.map((c) => c.id), opts.productVariantGids!, ) : new Set(candidates.map((c) => c.id)); const eligibleIds = new Set( filterPickupLocationIds({ candidates, pickupTemplateLocationIds, stockedLocationIds, }), ); return candidates .filter((c) => eligibleIds.has(c.id)) .map((c) => ({ id: c.id, name: c.name, address: c.address, lat: c.lat, lng: c.lng })); }