@zorgo/next 1.3.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/cli.d.mts +9 -0
  2. package/dist/cli.d.ts +9 -0
  3. package/dist/cli.js +44 -66
  4. package/dist/cli.js.map +1 -1
  5. package/dist/cli.mjs +43 -65
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/components.d.mts +735 -5
  8. package/dist/components.d.ts +735 -5
  9. package/dist/components.js +3692 -525
  10. package/dist/components.js.map +1 -1
  11. package/dist/components.mjs +3663 -540
  12. package/dist/components.mjs.map +1 -1
  13. package/dist/images.d.mts +25 -13
  14. package/dist/images.d.ts +25 -13
  15. package/dist/images.js +18 -8
  16. package/dist/images.js.map +1 -1
  17. package/dist/images.mjs +15 -8
  18. package/dist/images.mjs.map +1 -1
  19. package/dist/index.d.mts +315 -9
  20. package/dist/index.d.ts +315 -9
  21. package/dist/index.js +58 -52
  22. package/dist/index.js.map +1 -1
  23. package/dist/index.mjs +58 -52
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/item-page.d.mts +6 -0
  26. package/dist/item-page.d.ts +6 -0
  27. package/dist/item-page.js +4 -2
  28. package/dist/item-page.js.map +1 -1
  29. package/dist/item-page.mjs +4 -2
  30. package/dist/item-page.mjs.map +1 -1
  31. package/dist/legal.d.mts +31 -0
  32. package/dist/legal.d.ts +31 -0
  33. package/dist/legal.js +322 -0
  34. package/dist/legal.js.map +1 -0
  35. package/dist/legal.mjs +293 -0
  36. package/dist/legal.mjs.map +1 -0
  37. package/dist/seo.d.mts +56 -1
  38. package/dist/seo.d.ts +56 -1
  39. package/dist/seo.js +111 -2
  40. package/dist/seo.js.map +1 -1
  41. package/dist/seo.mjs +106 -1
  42. package/dist/seo.mjs.map +1 -1
  43. package/dist/server.d.mts +27 -1
  44. package/dist/server.d.ts +27 -1
  45. package/dist/server.js +322 -102
  46. package/dist/server.js.map +1 -1
  47. package/dist/server.mjs +322 -104
  48. package/dist/server.mjs.map +1 -1
  49. package/dist/slugs.d.mts +2 -1
  50. package/dist/slugs.d.ts +2 -1
  51. package/dist/slugs.js +2 -0
  52. package/dist/slugs.js.map +1 -1
  53. package/dist/slugs.mjs +1 -0
  54. package/dist/tree/README.md +2 -2
  55. package/dist/tree/app/error.tsx +12 -3
  56. package/dist/tree/app/global-error.tsx +21 -4
  57. package/dist/tree/app/globals.css +41 -0
  58. package/dist/tree/app/not-found.tsx +17 -3
  59. package/dist/tree/app/privacy/page.tsx.gen.ts +28 -9
  60. package/dist/tree/app/terms/page.tsx.gen.ts +26 -10
  61. package/dist/tree/gitignore +6 -1
  62. package/dist/tree/next.config.ts +6 -5
  63. package/dist/tree/open-next.config.ts +7 -0
  64. package/dist/tree/package.json.gen.ts +8 -2
  65. package/package.json +7 -2
  66. package/dist/tree/app/globals.css.gen.ts +0 -124
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts","../src/seo.ts","../src/internal/itemImages.ts","../../zorgo/universal/constants.ts","../../zorgo/universal/utils/menuUtils.ts","../src/images.ts"],"sourcesContent":["// Server-only entry (@zorgo/next/server). Node/Server-Component safe: no React,\n// no \"use client\", no client hooks — so scripts/prebuild.ts and the\n// app/api/zorgo-token route can import the token-exchange and build pipeline\n// from the package instead of scaffolding their own copies. Shipping these here\n// means a package update fixes every site, rather than each site freezing a\n// forked copy at scaffold time.\nimport { writeFileSync, mkdirSync } from \"fs\";\nimport path from \"path\";\nimport { loadEnvConfig } from \"@next/env\";\nimport { z } from \"zod\";\nimport type { Menu } from \"@zorgo/universal/utils/menuUtils\";\nimport { buildRestaurantJsonLd } from \"./seo\";\nimport { stampItemImages } from \"./internal/itemImages\";\n\n// localhost fallback keeps a stray dev run from silently hitting production.\n// Read at call time, never at module load: prebuild imports this file before it\n// calls loadEnvConfig(), and ES imports are evaluated first — a module-level\n// read would freeze to the fallback before .env is loaded.\nfunction apiBaseUrl(): string {\n\treturn process.env.NEXT_PUBLIC_API_BASE_URL ?? \"http://localhost:3000\";\n}\n\n/** Why a site token could not be obtained — lets callers report precisely. */\nexport type SiteTokenFailure = \"missing-secret\" | \"rejected\" | \"unreachable\";\n\nexport class SiteTokenError extends Error {\n\treadonly reason: SiteTokenFailure;\n\tconstructor(reason: SiteTokenFailure, message: string, options?: { cause?: unknown }) {\n\t\tsuper(message, options);\n\t\tthis.name = \"SiteTokenError\";\n\t\tthis.reason = reason;\n\t}\n}\n\n/**\n * Exchange ZORGO_SITE_SECRET for a short-lived site token.\n *\n * Throws a {@link SiteTokenError} that distinguishes the three failure modes\n * (secret not set / endpoint rejected the secret / endpoint unreachable) rather\n * than collapsing them into one misleading \"missing secret\" message.\n */\nexport async function fetchSiteToken(): Promise<string> {\n\tconst secret = process.env.ZORGO_SITE_SECRET;\n\tif (!secret) {\n\t\tthrow new SiteTokenError(\n\t\t\t\"missing-secret\",\n\t\t\t\"ZORGO_SITE_SECRET is not set. Add it to this site's .env.\",\n\t\t);\n\t}\n\n\tconst url = `${apiBaseUrl()}/auth/site-token`;\n\tlet res: Response;\n\ttry {\n\t\tres = await fetch(url, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: { \"x-zorgo-site-secret\": secret },\n\t\t});\n\t} catch (cause) {\n\t\tthrow new SiteTokenError(\n\t\t\t\"unreachable\",\n\t\t\t`Could not reach the site-token endpoint at ${url}. Check NEXT_PUBLIC_API_BASE_URL and that the API is running.`,\n\t\t\t{ cause },\n\t\t);\n\t}\n\n\tif (!res.ok) {\n\t\tconst body = (await res.text().catch(() => \"\")).slice(0, 200);\n\t\tthrow new SiteTokenError(\n\t\t\t\"rejected\",\n\t\t\t`The site-token endpoint rejected the request (${res.status} ${res.statusText}) at ${url}. ` +\n\t\t\t\t`This usually means ZORGO_SITE_SECRET is wrong for this environment.${body ? ` Response: ${body}` : \"\"}`,\n\t\t);\n\t}\n\n\tconst { token } = (await res.json()) as { token?: string };\n\tif (!token) {\n\t\tthrow new SiteTokenError(\n\t\t\t\"rejected\",\n\t\t\t`The site-token endpoint returned 200 but no token at ${url}.`,\n\t\t);\n\t}\n\treturn token;\n}\n\n// ---------------------------------------------------------------------------\n// Prebuild pipeline\n// ---------------------------------------------------------------------------\n// Everything below runs at build time in a scaffolded site's cwd, fetching\n// franchise data and writing the generated modules/assets the site imports.\n\ninterface FetchArgs {\n\tfranchiseId: number;\n\ttoken: string;\n}\n\nconst BrandingSchema = z\n\t.object({\n\t\tbackgroundBase: z.string().default(\"#F5F5F5\"),\n\t\tprimary: z.string().default(\"#3264FF\"),\n\t\taccent: z.string().default(\"#9696FF\"),\n\t\tfontFamily: z.string().default(\"Inter\"), // Google Font family name\n\t})\n\t.prefault({});\ntype Branding = z.infer<typeof BrandingSchema>;\n\n// A freshly-created franchise commonly has null columns (no site_url set yet,\n// name not filled in). Coerce null|undefined to sensible values instead of\n// letting z.string() throw on null and blow up JSON-LD generation.\nconst FranchiseSchema = z\n\t.object({\n\t\tname: z\n\t\t\t.string()\n\t\t\t.nullish()\n\t\t\t.transform((v) => v ?? process.env.ZORGO_PROJECT_NAME ?? \"Restaurant\"),\n\t\tsite_url: z\n\t\t\t.string()\n\t\t\t.nullish()\n\t\t\t.transform((v) => v ?? undefined),\n\t})\n\t.prefault({});\n\nconst HoursSchema = z.array(z.unknown()).prefault([]);\n\nconst MenuSchema = z.object({\n\titems: z.array(z.unknown()).default([]),\n\tcategories: z.array(z.unknown()).default([]),\n\tcomponents: z.array(z.unknown()).default([]),\n\trules: z.array(z.unknown()).default([]),\n});\n\nasync function fetchData<T>(\n\tendpoint: string,\n\ttoken: string,\n\tschema: z.ZodType<T>,\n): Promise<T> {\n\ttry {\n\t\tconst res = await fetch(`${apiBaseUrl()}${endpoint}`, {\n\t\t\theaders: { Authorization: `Bearer ${token}` },\n\t\t});\n\t\tif (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);\n\t\tconst body = await res.json();\n\t\treturn schema.parse(body?.data ?? body);\n\t} catch (err) {\n\t\tconsole.error(`prebuild: ${endpoint} failed, using defaults:`, err);\n\t\treturn schema.parse(undefined);\n\t}\n}\n\n// style stuff\nconst FONT_ON_LIGHT = \"#0a0a0a\";\nconst FONT_ON_DARK = \"#f5f5f5\";\n/** Neutral channel delta toward white (elevated) / black (recessed). */\nconst BACKGROUND_SURFACE_DELTA = 15;\n/**\n * Max warm/cool bias via R↔B trade (full strength on light bases).\n * Scaled down by luminance so darker colors shift less.\n */\nconst BACKGROUND_TEMPERATURE_BIAS = 8;\n\nfunction parseHexRgb(background: string): { r: number; g: number; b: number } | null {\n\tconst hex = background.trim().replace(/^#/, \"\");\n\tconst full =\n\t\thex.length === 3\n\t\t\t? hex\n\t\t\t\t\t.split(\"\")\n\t\t\t\t\t.map((c) => c + c)\n\t\t\t\t\t.join(\"\")\n\t\t\t: hex;\n\tif (full.length !== 6 || Number.isNaN(Number.parseInt(full, 16))) {\n\t\treturn null;\n\t}\n\treturn {\n\t\tr: Number.parseInt(full.slice(0, 2), 16),\n\t\tg: Number.parseInt(full.slice(2, 4), 16),\n\t\tb: Number.parseInt(full.slice(4, 6), 16),\n\t};\n}\n\nfunction toHex({ r, g, b }: { r: number; g: number; b: number }): string {\n\treturn `#${[r, g, b].map((c) => c.toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n\nfunction relativeLuminance({ r, g, b }: { r: number; g: number; b: number }): number {\n\treturn (0.299 * r + 0.587 * g + 0.114 * b) / 255;\n}\n\n/** Temperature bias scaled by luminance; max on light bases, near-zero on dark. */\nfunction temperatureBiasFor({ r, g, b }: { r: number; g: number; b: number }): number {\n\treturn Math.round(BACKGROUND_TEMPERATURE_BIAS * relativeLuminance({ r, g, b }));\n}\n\n/** Off-black on light backgrounds, off-white on dark. */\nfunction deriveFontColor(background: string): string {\n\tconst rgb = parseHexRgb(background);\n\tif (!rgb) return FONT_ON_LIGHT;\n\treturn relativeLuminance(rgb) > 0.5 ? FONT_ON_LIGHT : FONT_ON_DARK;\n}\n\n/** Lighter, slightly warmer surface (cards, raised panels) from backgroundBase. */\nfunction deriveElevatedBackground(background: string): string {\n\tconst rgb = parseHexRgb(background);\n\tif (!rgb) return background;\n\tconst bias = temperatureBiasFor(rgb);\n\tconst warm = Math.round(bias / 4);\n\treturn toHex({\n\t\tr: Math.min(255, rgb.r + BACKGROUND_SURFACE_DELTA + warm),\n\t\tg: Math.min(255, rgb.g + BACKGROUND_SURFACE_DELTA),\n\t\tb: Math.min(255, rgb.b + BACKGROUND_SURFACE_DELTA - bias),\n\t});\n}\n\n/** Darker, slightly cooler surface (wells, inset areas) from backgroundBase. */\nfunction deriveRecessedBackground(background: string): string {\n\tconst rgb = parseHexRgb(background);\n\tif (!rgb) return background;\n\tconst bias = temperatureBiasFor(rgb);\n\tconst cool = Math.round(bias / 4);\n\treturn toHex({\n\t\tr: Math.max(0, rgb.r - BACKGROUND_SURFACE_DELTA - cool),\n\t\tg: Math.max(0, rgb.g - BACKGROUND_SURFACE_DELTA),\n\t\tb: Math.max(0, rgb.b - BACKGROUND_SURFACE_DELTA + bias),\n\t});\n}\n\nfunction writeWranglerToml() {\n\tconst name = process.env.ZORGO_PROJECT_NAME ?? \"zorgo-site\";\n\tconst siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? \"\";\n\tconst apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL ?? \"\";\n\tconst baseUrl = process.env.NEXT_PUBLIC_BASE_URL ?? siteUrl;\n\tconst supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL ?? \"\";\n\n\t// custom_domain routes need a bare hostname; skip them if the site URL isn't\n\t// a parseable absolute URL (e.g. a local/placeholder value).\n\tlet host: string | undefined;\n\ttry {\n\t\thost = new URL(siteUrl).hostname;\n\t} catch {\n\t\thost = undefined;\n\t}\n\tconst routes = host\n\t\t? `\n[[routes]]\npattern = \"${host}\"\ncustom_domain = true\n\n[[routes]]\npattern = \"www.${host}\"\ncustom_domain = true\n`\n\t\t: \"\";\n\n\t// Omit empty values: opennextjs-cloudflare injects [vars] into the build env, so\n\t// an empty NEXT_PUBLIC_SITE_URL (a site piloted before its URL is set) would land\n\t// as \"\" — a defined-but-empty value that slips past the `?? localhost` fallbacks in\n\t// layout/robots/sitemap and makes new URL(\"\") throw, failing the build/deploy.\n\t// Leaving the var out keeps it `undefined` so those fallbacks fire.\n\tconst vars: Record<string, string> = {\n\t\tNEXT_PUBLIC_SUPABASE_URL: supabaseUrl,\n\t\tNEXT_PUBLIC_API_BASE_URL: apiUrl,\n\t\tNEXT_PUBLIC_SITE_URL: siteUrl,\n\t\tNEXT_PUBLIC_BASE_URL: baseUrl,\n\t};\n\tconst varsBlock = Object.entries(vars)\n\t\t.filter(([, value]) => value !== \"\")\n\t\t.map(([key, value]) => `${key} = \"${value}\"`)\n\t\t.join(\"\\n\");\n\n\tconst toml = `# AUTO-GENERATED by scripts/prebuild.ts from .env — do not edit.\nname = \"${name}\"\nmain = \".open-next/worker.js\"\ncompatibility_date = \"2024-09-23\"\ncompatibility_flags = [\"nodejs_compat\"]\n\n# Bundles the Cloudflare worker as part of \\`wrangler deploy\\` (and \\`wrangler dev\\`),\n# so the deploy command stays a plain \\`wrangler deploy\\`. prebuild (which writes\n# this file + lib/generated) must still run first — on Cloudflare Workers Builds\n# that is the project's Build command; locally it is the npm scripts below.\n[build]\ncommand = \"npx opennextjs-cloudflare build\"\n\n[assets]\ndirectory = \".open-next/assets\"\nbinding = \"ASSETS\"\n\n[vars]\n${varsBlock}\n\n[observability]\nenabled = true\n\n[observability.logs]\nenabled = true\ninvocation_logs = true\n${routes}`;\n\n\twriteFileSync(path.join(process.cwd(), \"wrangler.toml\"), toml);\n}\n\nasync function logo({ franchiseId, token }: FetchArgs) {\n\tconst logoResponse = await fetch(\n\t\t`${apiBaseUrl()}/franchise-info/logo/${franchiseId}`,\n\t\t{ headers: { Authorization: `Bearer ${token}` } },\n\t);\n\n\t// 404 (no logo) returns a JSON body — don't persist it as logo.png, or the\n\t// icon/OG image would render an error blob instead of falling back.\n\tif (!logoResponse.ok) return;\n\n\tconst arrayBuffer = await logoResponse.arrayBuffer();\n\tconst buffer = Buffer.from(arrayBuffer);\n\n\tconst outDir = path.join(process.cwd(), `public/images/`);\n\tmkdirSync(outDir, { recursive: true });\n\twriteFileSync(path.join(outDir, \"logo.png\"), buffer);\n\twriteFileSync(path.join(outDir, \"logo.jpg\"), buffer);\n}\n\n// Embedding vectors are per-item and huge; they'd ship in the client bundle for\n// nothing. ponytail: name-based drop, revisit if the API starts returning the\n// menu trimmed already.\nconst dropHeavyColumns = (key: string, value: unknown) =>\n\tkey.endsWith(\"_vector\") || key.endsWith(\"_embedding\") ? undefined : value;\n\nfunction writeMenuModule(menu: Menu) {\n\tconst outDir = path.join(process.cwd(), `lib/generated/`);\n\tmkdirSync(outDir, { recursive: true });\n\t// typed module so consumers get Menu-checked imports instead of an\n\t// untyped runtime fetch of menu.json.\n\t//\n\t// The API returns whole DB rows, which carry columns the interfaces don't\n\t// declare (created_at, status_id, embeddings...). Annotating the literal\n\t// directly triggers TS excess-property checking and fails the site build\n\t// every time a column is added, so assert instead — the shape is validated\n\t// server-side, and consumers still import a typed `Menu`.\n\twriteFileSync(\n\t\tpath.join(outDir, \"menu.ts\"),\n\t\t`// AUTO-GENERATED by scripts/prebuild.ts — do not edit.\\n` +\n\t\t\t`import type { Menu } from \"@zorgo/next\";\\n\\n` +\n\t\t\t`export const menu = ${JSON.stringify(menu, dropHeavyColumns, 2)} as unknown as Menu;\\n`,\n\t);\n}\n\nasync function menu({ franchiseId, token }: FetchArgs): Promise<Menu> {\n\tconst menuResponse = await fetch(\n\t\t`${apiBaseUrl()}/franchise-info/menu/${franchiseId}`,\n\t\t{ headers: { Authorization: `Bearer ${token}` } },\n\t);\n\n\t// A non-ok response is a real failure (auth, server error) — fail loudly.\n\tif (!menuResponse.ok) {\n\t\tthrow new Error(\n\t\t\t`menu fetch failed ${menuResponse.status}: ${await menuResponse.text()}`,\n\t\t);\n\t}\n\n\tconst body = await menuResponse.json();\n\tconst parsed = MenuSchema.parse(body?.data ?? {}) as Menu;\n\n\t// An empty menu is expected for a freshly-initialized franchise that hasn't\n\t// added items yet. Ship a valid empty menu.ts and keep building — the\n\t// storefront renders its empty state instead of failing the whole build.\n\tif (parsed.items.length === 0) {\n\t\tconsole.warn(\n\t\t\t\"prebuild: franchise has no menu items yet — generating an empty menu. \" +\n\t\t\t\t\"Add items in the dashboard and rebuild to populate the storefront.\",\n\t\t);\n\t}\n\n\t// Must precede writeMenuModule: it stamps local_key onto the image records\n\t// that get serialized into menu.ts.\n\tawait stampItemImages(parsed, process.cwd());\n\n\twriteMenuModule(parsed);\n\treturn parsed;\n}\n\n// franchise row (name, site_url) -> Restaurant JSON-LD, written as a typed\n// module the layout imports and drops into <head>.\nasync function jsonLd({ franchiseId, token }: FetchArgs, menuData: Menu) {\n\tconst franchise = await fetchData(\n\t\t`/franchise-info/franchise/${franchiseId}`,\n\t\ttoken,\n\t\tFranchiseSchema,\n\t);\n\tconst url = franchise.site_url;\n\n\tconst data = buildRestaurantJsonLd(menuData, {\n\t\tname: franchise.name,\n\t\turl,\n\t\timage: url ? `${url}/images/logo.png` : undefined,\n\t});\n\n\tconst outDir = path.join(process.cwd(), `lib/generated/`);\n\tmkdirSync(outDir, { recursive: true });\n\twriteFileSync(\n\t\tpath.join(outDir, \"jsonld.ts\"),\n\t\t`// AUTO-GENERATED by scripts/prebuild.ts — do not edit.\\n` +\n\t\t\t`export const restaurantJsonLd = ${JSON.stringify(data, null, 2)};\\n`,\n\t);\n}\n\nasync function hours({ franchiseId, token }: FetchArgs) {\n\tawait fetchData(`/franchise-info/hours/${franchiseId}`, token, HoursSchema);\n}\n\nasync function styles({ franchiseId, token }: FetchArgs) {\n\tconst brand = await fetchData(\n\t\t`/franchise-info/styles/${franchiseId}`,\n\t\ttoken,\n\t\tBrandingSchema,\n\t);\n\n\t// Same derived palette globals.css uses, persisted as JSON so the build-time\n\t// icon/OG/manifest routes stay on brand (they readFileSync this).\n\tconst branding = {\n\t\t...brand,\n\t\tbackgroundElevated: deriveElevatedBackground(brand.backgroundBase),\n\t\tbackgroundRecessed: deriveRecessedBackground(brand.backgroundBase),\n\t\tforeground: deriveFontColor(brand.backgroundBase),\n\t\tprimaryForeground: deriveFontColor(brand.primary),\n\t\taccentForeground: deriveFontColor(brand.accent),\n\t};\n\n\tconst stylesDir = path.join(process.cwd(), `public/styles/`);\n\tmkdirSync(stylesDir, { recursive: true });\n\twriteFileSync(\n\t\tpath.join(stylesDir, \"styles.json\"),\n\t\tJSON.stringify(branding, null, 2),\n\t);\n\n\twriteFileSync(\n\t\tpath.join(process.cwd(), \"app\", \"globals.css\"),\n\t\tbuildGlobalsCss(brand),\n\t);\n}\n\nfunction buildGlobalsCss(branding: Branding): string {\n\tconst fontFamilyParam = branding.fontFamily.replace(/ /g, \"+\");\n\tconst foreground = deriveFontColor(branding.backgroundBase);\n\tconst primaryForeground = deriveFontColor(branding.primary);\n\tconst accentForeground = deriveFontColor(branding.accent);\n\tconst backgroundElevated = deriveElevatedBackground(branding.backgroundBase);\n\tconst backgroundRecessed = deriveRecessedBackground(branding.backgroundBase);\n\n\treturn `\n@import url(\"https://fonts.googleapis.com/css2?family=${fontFamilyParam}:wght@400;700&display=swap\");\n@import \"tailwindcss\";\n/* Tailwind v4 skips node_modules; @zorgo/next ships un-obfuscated components so\n their class names are scannable here. */\n@source \"../node_modules/@zorgo/next/dist/components.mjs\";\n\n:root {\n --background: ${branding.backgroundBase};\n --background-elevated: ${backgroundElevated};\n --background-recessed: ${backgroundRecessed};\n --foreground: ${foreground};\n --primary: ${branding.primary};\n --primary-foreground: ${primaryForeground};\n --accent: ${branding.accent};\n --accent-foreground: ${accentForeground};\n --font-sans: \"${branding.fontFamily}\", sans-serif;\n}\n\n@theme inline {\n --color-background: var(--background);\n --color-background-elevated: var(--background-elevated);\n --color-background-recessed: var(--background-recessed);\n --color-foreground: var(--foreground);\n --color-primary: var(--primary);\n --color-primary-foreground: var(--primary-foreground);\n --color-accent: var(--accent);\n --color-accent-foreground: var(--accent-foreground);\n --font-sans: var(--font-sans);\n}\n\n@layer base {\n button,\n [type=\"button\"],\n [type=\"submit\"],\n [type=\"reset\"] {\n cursor: pointer;\n }\n\n button:disabled,\n [type=\"button\"]:disabled,\n [type=\"submit\"]:disabled,\n [type=\"reset\"]:disabled {\n cursor: not-allowed;\n }\n}\n`;\n}\n\n/**\n * Full prebuild pipeline for a scaffolded site. Loads the site's .env, exchanges\n * the site secret for a token, then fetches franchise data and writes the\n * generated modules/assets (wrangler.toml, menu.ts, jsonld.ts, styles, logo).\n * Called from the site's thin scripts/prebuild.ts wrapper.\n */\nexport async function runPrebuild(): Promise<void> {\n\tconsole.log(\"running prebuild script...\");\n\n\t// Load .env here so the wrapper stays a one-liner and can't reintroduce the\n\t// import-order bug (env must be loaded before any env read below).\n\tloadEnvConfig(process.cwd());\n\n\t// regenerate deploy config from .env before any network work\n\twriteWranglerToml();\n\n\tconst token = await fetchSiteToken();\n\n\tconst encodedPayload = token.split(\".\")[1];\n\tif (!encodedPayload) {\n\t\tthrow new Error(\"site token is not a well-formed JWT (no payload segment)\");\n\t}\n\tconst payload = JSON.parse(Buffer.from(encodedPayload, \"base64url\").toString());\n\tconst franchiseId = payload.franchise_ids[0];\n\n\t// menu + franchise feed the JSON-LD; a menu *fetch* failure breaks the build\n\t// loudly, but an empty menu (new franchise) is tolerated inside menu().\n\tconst [menuData] = await Promise.all([\n\t\tmenu({ franchiseId, token }),\n\t\tlogo({ franchiseId, token }).catch((err) => console.error(\"logo failed:\", err)),\n\t\thours({ franchiseId, token }).catch((err) => console.error(\"hours failed:\", err)),\n\t\tstyles({ franchiseId, token }).catch((err) => console.error(\"styles failed:\", err)),\n\t]);\n\n\tawait jsonLd({ franchiseId, token }, menuData).catch((err) =>\n\t\tconsole.error(\"jsonLd failed:\", err),\n\t);\n\n\tconsole.log(\"prebuild script completed\");\n}\n","import type { Menu } from \"@zorgo/universal/utils/menuUtils\";\nimport type { ItemVariant, MenuItem } from \"@zorgo/universal/interfaces\";\n\n/** Franchise-level facts for the Restaurant node. Everything but `name` is\n * optional so the schema degrades gracefully when a field is missing. */\nexport interface RestaurantInfo {\n\tname: string;\n\turl?: string;\n\timage?: string;\n\tdescription?: string;\n\tservesCuisine?: string[];\n}\n\n// cents -> \"5.49\" (schema.org Offer.price is a string in major currency units)\nfunction priceString(cents: number): string {\n\treturn (cents / 100).toFixed(2);\n}\n\nfunction variantOffer(variant: ItemVariant) {\n\treturn {\n\t\t\"@type\": \"Offer\",\n\t\tprice: priceString(variant.display_price ?? variant.price),\n\t\tpriceCurrency: \"USD\",\n\t};\n}\n\nfunction menuItemNode(item: MenuItem) {\n\tconst node: Record<string, unknown> = {\n\t\t\"@type\": \"MenuItem\",\n\t\tname: item.name,\n\t};\n\tif (item.description) node.description = item.description;\n\t// one Offer for a single price, an array when the item has size/variant prices\n\tconst offers = item.variants.map(variantOffer);\n\tif (offers.length === 1) node.offers = offers[0];\n\telse if (offers.length > 1) node.offers = offers;\n\treturn node;\n}\n\n/**\n * Builds a standalone schema.org JSON-LD object for a single menu item, for\n * embedding in that item's `/menu/[slug]` page `<head>`. Reuses `menuItemNode`\n * (the same node shape used inside the Restaurant menu) and adds the\n * `@context` a top-level node needs. Pure — safe to run at build time.\n */\nexport function buildMenuItemJsonLd(item: MenuItem) {\n\treturn { \"@context\": \"https://schema.org\", ...menuItemNode(item) };\n}\n\n/**\n * Builds a schema.org Restaurant JSON-LD object from a prebuilt Menu and\n * franchise info. Categories become MenuSections; the items in each become\n * MenuItems. Pure — same inputs, same output — so it can run at prebuild time\n * and be embedded straight into the site's <head>.\n */\nexport function buildRestaurantJsonLd(menu: Menu, info: RestaurantInfo) {\n\tconst hasMenuSection = menu.categories.map((category) => {\n\t\tconst section: Record<string, unknown> = {\n\t\t\t\"@type\": \"MenuSection\",\n\t\t\tname: category.name,\n\t\t};\n\t\tconst items = menu.items\n\t\t\t.filter((item) => item.category_id === category.category_id)\n\t\t\t.map(menuItemNode);\n\t\tif (items.length) section.hasMenuItem = items;\n\t\treturn section;\n\t});\n\n\tconst restaurant: Record<string, unknown> = {\n\t\t\"@context\": \"https://schema.org\",\n\t\t\"@type\": \"Restaurant\",\n\t\tname: info.name,\n\t};\n\tif (info.url) restaurant.url = info.url;\n\tif (info.image) restaurant.image = info.image;\n\tif (info.description) restaurant.description = info.description;\n\tif (info.servesCuisine?.length) restaurant.servesCuisine = info.servesCuisine;\n\trestaurant.hasMenu = {\n\t\t\"@type\": \"Menu\",\n\t\tname: \"Menu\",\n\t\thasMenuSection,\n\t};\n\n\treturn restaurant;\n}\n","// Prebuild-only: downloads each menu item image from the public bucket and emits\n// a fixed AVIF width ladder into public/images/items/, stamping `local_key` onto\n// the image record so the generated menu module points at the local variants.\n// Runs in the site's cwd during scripts/prebuild.ts — never in the browser.\nimport { existsSync, mkdirSync } from \"fs\";\nimport { createRequire } from \"module\";\nimport path from \"path\";\nimport type { BaseItemImage } from \"@zorgo/universal/interfaces\";\nimport type { Menu } from \"@zorgo/universal/utils/menuUtils\";\nimport { menuImageUrl } from \"@zorgo/universal/utils/menuUtils\";\nimport { ITEM_IMAGE_PREFIX, ITEM_IMAGE_WIDTHS } from \"../images\";\n\n/**\n * Encoder and loader share one width list — see images.ts. Changing it requires\n * regenerating every site's variants, or the loader requests a rung that was\n * never written.\n */\nexport { ITEM_IMAGE_WIDTHS };\n\n/**\n * AVIF only: a custom next/image loader returns a single URL and so cannot do\n * Accept-header format negotiation. AVIF is the smaller of the two and is\n * universally supported in current browsers; anything older falls back to the\n * bucket URL only when the whole download failed, not per-format.\n */\nconst AVIF_QUALITY = 60;\n\n/**\n * sharp is pinned in the site's package.json, not this package's, so a bare\n * `import(\"sharp\")` resolves from wherever this bundle happens to live and fails\n * under pnpm's isolated linker. Resolve it from the site root instead — the same\n * thing Next does for its own optimizer.\n */\nfunction loadSharp(root: string): typeof import(\"sharp\").default {\n\t// require, not import(): a computed dynamic import is a module graph edge\n\t// bundlers try to follow, and the site's api/zorgo-token route imports this\n\t// bundle for fetchSiteToken — Turbopack then fails the route on a `sharp` it\n\t// has no reason to resolve. A createRequire call is opaque to them, and this\n\t// line only ever runs in Node during prebuild.\n\tfor (const from of [root, process.cwd()]) {\n\t\ttry {\n\t\t\treturn createRequire(path.join(from, \"package.json\"))(\"sharp\");\n\t\t} catch (err) {\n\t\t\tif ((err as NodeJS.ErrnoException).code !== \"MODULE_NOT_FOUND\") throw err;\n\t\t}\n\t}\n\tthrow new Error(\n\t\t`sharp is not installed under ${root}. It is pinned in every scaffolded site's package.json — run an install there.`,\n\t);\n}\n\n/**\n * Storage path -> a filesystem-safe key that changes whenever the underlying\n * object does. Upload keys are timestamped, so a re-upload yields a new key and\n * can never collide with the previous image's cached variants.\n */\nexport function itemImageLocalKey(image: BaseItemImage): string | null {\n\tconst storagePath = image.processed_path ?? image.raw_path;\n\tif (!storagePath) return null;\n\treturn storagePath\n\t\t.replace(/\\.[^./]+$/, \"\")\n\t\t.replace(/[^a-zA-Z0-9_]+/g, \"-\")\n\t\t.replace(/^-|-$/g, \"\");\n}\n\n/** Absolute path of one variant file. */\nexport function itemImageVariantPath(root: string, key: string, width: number): string {\n\treturn path.join(root, \"public\", ITEM_IMAGE_PREFIX, `${key}-${width}.avif`);\n}\n\n/**\n * Downloads one image and writes its ladder. Returns the key on success, or\n * null on any failure — the caller leaves `local_key` unset and the render\n * falls back to the bucket URL, so a dead image never fails the build.\n *\n * Variants already on disk are kept: keys are content-addressed by upload\n * timestamp, so an existing file is always the right file.\n */\nexport async function materializeItemImage(\n\timage: BaseItemImage,\n\troot: string,\n): Promise<string | null> {\n\tconst key = itemImageLocalKey(image);\n\tif (!key) return null;\n\n\tconst targets = ITEM_IMAGE_WIDTHS.map((width) => ({\n\t\twidth,\n\t\tfile: itemImageVariantPath(root, key, width),\n\t}));\n\tif (targets.every(({ file }) => existsSync(file))) return key;\n\n\tconst url = menuImageUrl(image, true);\n\tif (!url) return null;\n\n\ttry {\n\t\tconst res = await fetch(url);\n\t\tif (!res.ok) throw new Error(`${res.status} ${res.statusText}`);\n\t\tconst source = Buffer.from(await res.arrayBuffer());\n\n\t\tconst sharp = loadSharp(root);\n\t\tmkdirSync(path.dirname(targets[0]!.file), { recursive: true });\n\t\tfor (const { width, file } of targets) {\n\t\t\t// withoutEnlargement: a source narrower than a rung is written at its\n\t\t\t// own width, so that rung's srcset descriptor overstates it slightly.\n\t\t\t// The browser then picks a wider candidate than it needs — wasteful in\n\t\t\t// bytes only when someone uploads a tiny image.\n\t\t\tawait sharp(source)\n\t\t\t\t.resize({ width, withoutEnlargement: true })\n\t\t\t\t.avif({ quality: AVIF_QUALITY })\n\t\t\t\t.toFile(file);\n\t\t}\n\t\treturn key;\n\t} catch (err) {\n\t\tconsole.error(`prebuild: image ${image.image_id} (${url}) failed:`, err);\n\t\treturn null;\n\t}\n}\n\n/**\n * Stamps `local_key` onto every image in the menu, in place, so writeMenuModule\n * serializes it. Mutates rather than copies because the menu is a passthrough\n * of unknown API columns — rebuilding it would mean re-describing that shape.\n */\nexport async function stampItemImages(menu: Menu, root: string): Promise<void> {\n\tconst images = menu.items.flatMap((item) => item.images ?? []);\n\tfor (const image of images) {\n\t\timage.local_key = await materializeItemImage(image, root);\n\t}\n\tconst stamped = images.filter((image) => image.local_key).length;\n\tif (images.length) {\n\t\tconsole.log(`prebuild: ${stamped}/${images.length} item images localized`);\n\t}\n}\n","export const SUPABASE_URL = \"https://dhnpoxorllfdobwftkyb.supabase.co\";\n\nexport const SUPABASE_ANON_KEY =\n\t\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRobnBveG9ybGxmZG9id2Z0a3liIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzkwNTQxMDgsImV4cCI6MjA1NDYzMDEwOH0.zI8PwbgKwijHZ1QmxJqmaPrFvg02w1vOY2Qcs7SddzI\";\nexport const ZORGO_PURPLE = \"rgb(153,67,233)\";\n\nexport const expoGoGoogleClientId =\n\t\"959654249401-dm6nbi27rj605ul1nkt8o2imb8f7daff.apps.googleusercontent.com\";\n\nexport const API_BASE_URL =\n\tprocess.env.NODE_ENV === \"production\"\n\t\t? \"https://api.zorgotech.com\"\n\t\t: \"http://localhost:3000\";\n\n\n// replace instances of API_BASE_URL with env enum\nexport enum Environments {\n\tProduction = \"https://api.zorgotech.com\",\n\tStaging = \"http://api.zorgo.ai\",\n\tDevelopment = \"http://localhost:3000\",\n}\n/** Emails allowed to access developer-only metrics (e.g. /performance/developer). */\nexport const DEVELOPER_EMAILS = [\"clawsorgo@gmail.com\", \"gmandwee@gmail.com\"] as const;\n\nexport function isDeveloperEmail(email?: string | null): boolean {\n\treturn !!email && (DEVELOPER_EMAILS as readonly string[]).includes(email);\n}\n\nexport const ORDER_STORE_CONSTANTS = {\n\tDEFAULTS: {\n\t\tPREP_MINUTES: 20, // default amount of time for prep\n\t\tPAST_HOURS: 24, // hours into the past orders are fetched for\n\t\tFUTURE_HOURS: 72 // hours into the future orders are fetched for \n\t},\n\tORDER_STATUS: {\n\t\tSTAGING: 10,\n\t\tPENDING: 1,\n\t\tCOMPLETED: 2,\n\t\tCANCELLED: 4,\n\t\tPAID: 6,\n\t\tPARTIALLY_PAID: 7,\n\t\tKITCHEN_DONE: 9,\n\t\tCONFIRMED: 11,\n\t\tPREPARING: 12,\n\t},\n\tPOS_METHOD_ID: 3,\n\tORDER_TYPE: {\n\t\tDINE_IN: 1,\n\t\tTAKEOUT: 2,\n\t\tDELIVERY: 3\n\t},\n\t/** KDS edit highlight duration after a POS modification. */\n\tMODIFICATION_HIGHLIGHT_MS: 8_000,\n\t/** POS order realtime: refetch + recycle postgres_changes channel on this cadence. */\n\tLISTENER_SYNC_INTERVAL_MS: 60_000,\n\tORDER_SELECT: `\n\t*,\n\n\tcharges (\n\t\t*\n\t),\n\n\tlocations (name),\n\n\tcustomers\n\t (customer_id, first_name, last_name, phone_number, email),\n\n\torder_items\n\t (\n\t\torder_item_id, item_id, quantity, price_at_order, order_id, comments, status, applied_promo_id, discount_amount,\n\t\titems (\n\t\t\titem_id, name, price,\n\t\t\tportion_schemes (id, franchise_id, geometry, name, min_divisions, max_divisions),\n\t\t\tbase_items (\n\t\t\t\tbase_item_id, category_id, name,\n\t\t\t\tcategories (category_id, name, is_catering)\n\t\t\t)\n\t\t),\n\t\torder_item_selected_items (\n\t\t\torder_item_id, rule_id, item_id, quantity, upcharge_price, portion,\n\t\t\titems (\n\t\t\t\titem_id, name, price,\n\t\t\t\tbase_items (\n\t\t\t\t\tbase_item_id, category_id, name,\n\t\t\t\t\tcategories (category_id, name, is_catering)\n\t\t\t\t)\n\t\t\t),\n\t\t\titem_rules (rule_id, group_name)\n\t\t),\n\t\torder_item_selected_components (\n\t\t\torder_item_id, rule_id, component_id, quantity, upcharge_price, portion,\n\t\t\tcomponents (id, name, component_type_id, serving_weight, measurement_type, display_unit), item_rules (rule_id, group_name)\n\t\t),\n\t\torder_item_components (\n\t\t\torder_item_id, component_id, quantity_servings, is_base, portion,\n\t\t\tcomponents (id, name, component_type_id, serving_weight, measurement_type, display_unit)\n\t\t)\n\t)\n`,\n};\n\nexport const CHARGE_STATUSES = {\n\tSUCCESS: 1,\n\tPENDING: 2,\n\tREFUNDED: 3,\n\tVOIDED: 4,\n\tPAYMENT_QUEUED: 5,\n\tFAILED: 6\n}\n","import { SupabaseClient } from \"@supabase/supabase-js\";\nimport { getDay, isAfter, isBefore, parse, startOfDay } from \"date-fns\";\nimport { SUPABASE_URL } from \"../constants\";\nimport {\n\tBaseItem,\n\tBaseItemImage,\n\tCategory,\n\tComponent,\n\tItemVariant,\n\tMenuItem,\n\tPlatforms,\n\tPromotion,\n\tRule,\n\tVisibilityConditionGroupAssignment,\n\tVisibilityEffect,\n} from \"../interfaces\";\nimport { csvFormat } from \"./misc\";\nimport {\n\tbuildVisibilityEvaluationContext,\n\tevaluateVisibilityCondition,\n} from \"./visibilityConditions\";\n\nexport interface Menu {\n\titems: MenuItem[];\n\tcategories: Category[];\n\tcomponents: Component[];\n\trules: Rule[];\n}\n\nexport interface MenuIds {\n\tbaseItemIds: number[];\n\tcategoryIds: number[];\n\titemIds: number[];\n}\n\ntype MenuIdRows = {\n\tcategories: Pick<Category, \"category_id\">[];\n\tbaseItems: Pick<BaseItem, \"base_item_id\" | \"category_id\">[];\n\titems: Pick<ItemVariant, \"item_id\" | \"base_item_id\">[];\n};\n\nexport async function getMenu(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n\texclusions: MenuIds = emptyMenuExclusions(),\n\tuseBehaviors = false,\n): Promise<Menu> {\n\tconst categories = await getCategories(\n\t\tsupabase,\n\t\tfranchiseId,\n\t\texclusions.categoryIds,\n\t);\n\n\tif (categories.length === 0) {\n\t\treturn buildMenu([], [], [], [], []);\n\t}\n\n\tconst baseItems = await getBaseItems(\n\t\tsupabase,\n\t\tfranchiseId,\n\t\texclusions.baseItemIds,\n\t\tcategories,\n\t);\n\n\tif (baseItems.length === 0) {\n\t\treturn buildMenu([], [], categories, [], []);\n\t}\n\n\tconst items = await getItems(supabase, baseItems, exclusions.itemIds);\n\tconst components = await getComponents(supabase, items, useBehaviors);\n\tconst rules = await getRules(supabase, baseItems, categories, items, useBehaviors);\n\n\treturn buildMenu(items, baseItems, categories, components, rules);\n}\n\nexport async function getCateringBaseItemIds(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n): Promise<number[]> {\n\tconst { data } = await supabase\n\t\t.from(\"base_items\")\n\t\t.select(\"base_item_id\")\n\t\t.eq(\"franchise_id\", franchiseId)\n\t\t.not(\"per_person\", \"is\", null)\n\t\t.throwOnError();\n\treturn data.map((row) => row.base_item_id);\n}\n\n/**\n * Attaches targeted promotions to menu cards without mutating the menu.\n * Order-wide promos intentionally stay out of menu display pricing.\n */\nexport function fitPromosToMenu(\n\tmenu: Menu,\n\tactivePromotions: Promotion[],\n): Menu {\n\tconst categoryPromoIds = new Map<number, number>();\n\tconst getDisplayPrice = (price: number, promo: Promotion) => {\n\t\tif (promo.promotion_type_id === 1) {\n\t\t\treturn Math.max(0, Math.floor(price * (1 - promo.discount_value / 100)));\n\t\t}\n\t\tif (promo.promotion_type_id === 2) {\n\t\t\treturn Math.max(0, price - promo.discount_value);\n\t\t}\n\t\treturn price;\n\t};\n\n\tconst items = menu.items.map((item) => {\n\t\t// Bulk discount promos (quantity-dependent sigmoid) attach to the item but never set a fixed display_price\n\t\tconst bulkPromo = activePromotions.find((promo) => {\n\t\t\tif (!promo.is_active || promo.id == null || !promo.discount_target_ids?.length || !promo.min_purchase_quantity) return false;\n\t\t\tconst targets = promo.discount_target_ids;\n\t\t\treturn (\n\t\t\t\t(promo.discount_target_type === \"category\" && targets.includes(item.category_id)) ||\n\t\t\t\t(promo.discount_target_type === \"item\" && targets.includes(item.base_item_id))\n\t\t\t);\n\t\t});\n\n\t\tlet itemPromoId: number | undefined = bulkPromo?.id;\n\n\t\tconst variants = item.variants.map((variant) => {\n\t\t\tlet bestPromo: Promotion | undefined;\n\t\t\tlet bestDisplayPrice = variant.price;\n\n\t\t\tfor (const promo of activePromotions) {\n\t\t\t\tif (\n\t\t\t\t\t!promo.is_active ||\n\t\t\t\t\tpromo.id == null ||\n\t\t\t\t\t!promo.discount_target_ids?.length ||\n\t\t\t\t\tpromo.min_purchase_amount || // order-level threshold — shown via modal, not baked into menu price\n\t\t\t\t\tpromo.min_purchase_quantity // quantity-dependent — handled above, no fixed display_price\n\t\t\t\t) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst targets = promo.discount_target_ids;\n\t\t\t\tconst applies =\n\t\t\t\t\t(promo.discount_target_type === \"category\" &&\n\t\t\t\t\t\ttargets.includes(item.category_id)) ||\n\t\t\t\t\t(promo.discount_target_type === \"item\" &&\n\t\t\t\t\t\ttargets.includes(item.base_item_id)) ||\n\t\t\t\t\t(promo.discount_target_type === \"item_variant\" &&\n\t\t\t\t\t\ttargets.includes(variant.item_id));\n\n\t\t\t\tif (!applies) continue;\n\n\t\t\t\tconst displayPrice = getDisplayPrice(variant.price, promo);\n\t\t\t\tif (displayPrice < bestDisplayPrice) {\n\t\t\t\t\tbestPromo = promo;\n\t\t\t\t\tbestDisplayPrice = displayPrice;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!bestPromo || bestDisplayPrice >= variant.price) {\n\t\t\t\treturn variant;\n\t\t\t}\n\n\t\t\titemPromoId ??= bestPromo.id!;\n\t\t\tif (bestPromo.discount_target_type === \"category\") {\n\t\t\t\tcategoryPromoIds.set(item.category_id, bestPromo.id!);\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\t...variant,\n\t\t\t\tactive_promo_id: bestPromo.id!,\n\t\t\t\tdisplay_price: bestDisplayPrice,\n\t\t\t};\n\t\t});\n\n\t\treturn { ...item, variants, active_promo_id: itemPromoId };\n\t});\n\n\tconst categories = menu.categories.map((category) => {\n\t\tconst { active_promo_id, ...cleanCategory } = category;\n\t\tvoid active_promo_id;\n\n\t\tconst promoId = categoryPromoIds.get(cleanCategory.category_id);\n\n\t\treturn promoId == null\n\t\t\t? cleanCategory\n\t\t\t: { ...cleanCategory, active_promo_id: promoId };\n\t});\n\n\treturn {\n\t\t...menu,\n\t\titems,\n\t\tcategories,\n\t};\n}\n\nexport async function getMenuExclusions(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n\tlocationId: number,\n\tplatform: Platforms,\n): Promise<MenuIds> {\n\tvoid locationId;\n\n\tconst menuIds = await getMenuIds(supabase, franchiseId);\n\tconst activeAssignments = await getActiveVisibilityAssignments(\n\t\tsupabase,\n\t\tfranchiseId,\n\t\tplatform,\n\t);\n\n\tif (activeAssignments.length === 0) {\n\t\treturn emptyMenuExclusions();\n\t}\n\n\tconst categoryEffects = buildTargetEffectMap(\n\t\tactiveAssignments,\n\t\t(assignment) => assignment.category_id,\n\t);\n\tconst baseItemEffects = buildTargetEffectMap(\n\t\tactiveAssignments,\n\t\t(assignment) => assignment.base_item_id,\n\t);\n\tconst itemEffects = buildTargetEffectMap(\n\t\tactiveAssignments,\n\t\t(assignment) => assignment.item_id,\n\t);\n\n\tconst baseItemById = new Map(\n\t\tmenuIds.baseItems.map((baseItem) => [baseItem.base_item_id, baseItem]),\n\t);\n\tconst itemById = new Map(menuIds.items.map((item) => [item.item_id, item]));\n\n\tconst isCategoryIncluded = (categoryId: number) =>\n\t\tcategoryEffects.get(categoryId) !== \"exclude\";\n\n\tconst isBaseItemIncluded = (baseItemId: number) => {\n\t\tconst effect = baseItemEffects.get(baseItemId);\n\t\tif (effect === \"include\") return true;\n\t\tif (effect === \"exclude\") return false;\n\n\t\tconst categoryId = baseItemById.get(baseItemId)?.category_id;\n\t\treturn categoryId == null || isCategoryIncluded(categoryId);\n\t};\n\n\tconst isItemIncluded = (itemId: number): boolean => {\n\t\tconst effect = itemEffects.get(itemId);\n\t\tif (effect === \"include\") return true;\n\t\tif (effect === \"exclude\") return false;\n\n\t\tconst item = itemById.get(itemId);\n\t\tif (item?.base_item_id == null) return true;\n\n\t\treturn isBaseItemIncluded(item.base_item_id);\n\t};\n\n\tconst includedItems = new Set(\n\t\tmenuIds.items\n\t\t\t.filter((item) => isItemIncluded(item.item_id))\n\t\t\t.map((item) => item.item_id),\n\t);\n\tconst includedBaseItems = new Set(\n\t\tmenuIds.baseItems\n\t\t\t.filter((baseItem) => isBaseItemIncluded(baseItem.base_item_id))\n\t\t\t.map((baseItem) => baseItem.base_item_id),\n\t);\n\n\tfor (const item of menuIds.items) {\n\t\tif (includedItems.has(item.item_id) && item.base_item_id != null) {\n\t\t\tincludedBaseItems.add(item.base_item_id);\n\t\t}\n\t}\n\n\tconst includedCategories = new Set(\n\t\tmenuIds.categories\n\t\t\t.filter((category) => isCategoryIncluded(category.category_id))\n\t\t\t.map((category) => category.category_id),\n\t);\n\n\tfor (const baseItem of menuIds.baseItems) {\n\t\tif (includedBaseItems.has(baseItem.base_item_id)) {\n\t\t\tincludedCategories.add(baseItem.category_id);\n\t\t}\n\t}\n\n\treturn {\n\t\tcategoryIds: menuIds.categories\n\t\t\t.map((category) => category.category_id)\n\t\t\t.filter((categoryId) => !includedCategories.has(categoryId)),\n\t\tbaseItemIds: menuIds.baseItems\n\t\t\t.map((baseItem) => baseItem.base_item_id)\n\t\t\t.filter((baseItemId) => !includedBaseItems.has(baseItemId)),\n\t\titemIds: menuIds.items\n\t\t\t.map((item) => item.item_id)\n\t\t\t.filter((itemId) => !includedItems.has(itemId)),\n\t};\n}\n\nexport function getNameFromId(\n\tid: number,\n\ttype: \"category\" | \"item\" | \"item_variant\",\n\tmenu: Partial<Menu>,\n): string {\n\tif (type === \"category\") {\n\t\tconst category = menu.categories?.find(\n\t\t\t(category) => category.category_id === id,\n\t\t);\n\n\t\tif (category?.name) {\n\t\t\treturn category.name;\n\t\t}\n\t} else if (type === \"item\") {\n\t\tconst item = menu.items?.find((item: any) => item.base_item_id === id);\n\n\t\tif (item?.name) {\n\t\t\treturn item.name;\n\t\t}\n\t} else if (type === \"item_variant\") {\n\t\tconst item = menu.items?.find((item: any) =>\n\t\t\titem.variants.find((variant: any) => variant.item_id === id),\n\t\t);\n\n\t\tif (item?.name) {\n\t\t\treturn item.name;\n\t\t}\n\t}\n\n\treturn id.toString(); // default to showing the item id\n}\n\nfunction emptyMenuExclusions(): MenuIds {\n\treturn {\n\t\tcategoryIds: [],\n\t\tbaseItemIds: [],\n\t\titemIds: [],\n\t};\n}\n\nasync function getMenuIds(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n): Promise<MenuIdRows> {\n\tconst { data: categories, error: categoriesError } = await supabase\n\t\t.from(\"categories\")\n\t\t.select(\"category_id\")\n\t\t.eq(\"franchise_id\", franchiseId);\n\n\tif (categoriesError) {\n\t\tthrow categoriesError;\n\t}\n\n\tconst { data: baseItems, error: baseItemsError } = await supabase\n\t\t.from(\"base_items\")\n\t\t.select(\"base_item_id, category_id\")\n\t\t.eq(\"franchise_id\", franchiseId);\n\n\tif (baseItemsError) {\n\t\tthrow baseItemsError;\n\t}\n\n\tconst baseItemIds = (baseItems ?? []).map((item) => item.base_item_id);\n\tconst { data: items, error: itemsError } =\n\t\tbaseItemIds.length > 0\n\t\t\t? await supabase\n\t\t\t\t.from(\"items\")\n\t\t\t\t.select(\"item_id, base_item_id\")\n\t\t\t\t.in(\"base_item_id\", baseItemIds)\n\t\t\t: { data: [], error: null };\n\n\tif (itemsError) {\n\t\tthrow itemsError;\n\t}\n\n\treturn {\n\t\tcategories: categories ?? [],\n\t\tbaseItems: baseItems ?? [],\n\t\titems: items ?? [],\n\t};\n}\n\nasync function getActiveVisibilityAssignments(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n\tplatform: Platforms,\n): Promise<VisibilityConditionGroupAssignment[]> {\n\tconst { data, error } = await supabase\n\t\t.from(\"visibility_condition_group_assignments\")\n\t\t.select(\"*, visibility_condition_groups (*)\")\n\t\t.eq(\"visibility_condition_groups.franchise_id\", franchiseId)\n\t\t.eq(\"visibility_condition_groups.platform\", platform);\n\n\tif (error || !data) {\n\t\treturn [];\n\t}\n\n\tconst context = buildVisibilityEvaluationContext({ platform });\n\n\treturn (data as VisibilityConditionGroupAssignment[]).filter((assignment) => {\n\t\tconst group = assignment.visibility_condition_groups;\n\n\t\tif (!group?.condition || group.platform !== context.platform) {\n\t\t\treturn false;\n\t\t}\n\n\t\ttry {\n\t\t\treturn evaluateVisibilityCondition(group.condition, context);\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t});\n}\n\nfunction buildTargetEffectMap(\n\tassignments: VisibilityConditionGroupAssignment[],\n\tselector: (\n\t\tassignment: VisibilityConditionGroupAssignment,\n\t) => number | null | undefined,\n): Map<number, VisibilityEffect> {\n\tconst effectsByTarget = new Map<number, VisibilityEffect[]>();\n\n\tfor (const assignment of assignments) {\n\t\tconst targetId = selector(assignment);\n\t\tif (targetId == null) continue;\n\n\t\teffectsByTarget.set(targetId, [\n\t\t\t...(effectsByTarget.get(targetId) ?? []),\n\t\t\tassignment.effect,\n\t\t]);\n\t}\n\n\treturn new Map(\n\t\tArray.from(effectsByTarget.entries()).map(([targetId, effects]) => [\n\t\t\ttargetId,\n\t\t\teffects.includes(\"exclude\") ? \"exclude\" : \"include\",\n\t\t]),\n\t);\n}\n\nasync function getCategories(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n\texcludedIds?: number[],\n): Promise<Category[]> {\n\tlet categoriesQuery = supabase\n\t\t.from(\"categories\")\n\t\t.select(\"*\")\n\t\t.eq(\"franchise_id\", franchiseId);\n\n\tif (excludedIds && excludedIds.length > 0) {\n\t\tcategoriesQuery = categoriesQuery.not(\n\t\t\t\"category_id\",\n\t\t\t\"in\",\n\t\t\tcsvFormat(excludedIds),\n\t\t);\n\t}\n\n\tconst { data: categories, error: categoriesError } = await categoriesQuery;\n\n\tif (categoriesError) {\n\t\tthrow categoriesError;\n\t}\n\n\treturn categories ?? [];\n}\n\nasync function getBaseItems(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n\texcludedIds?: number[],\n\tcategories?: Category[],\n): Promise<BaseItem[]> {\n\tif (categories && categories.length === 0) {\n\t\treturn [];\n\t}\n\n\tlet baseItemsQuery = supabase\n\t\t.from(\"base_items\")\n\t\t.select(\"*\")\n\t\t.eq(\"franchise_id\", franchiseId);\n\n\tif (excludedIds && excludedIds.length > 0) {\n\t\tbaseItemsQuery = baseItemsQuery.not(\n\t\t\t\"base_item_id\",\n\t\t\t\"in\",\n\t\t\tcsvFormat(excludedIds),\n\t\t);\n\t}\n\n\tif (categories) {\n\t\tbaseItemsQuery = baseItemsQuery.in(\n\t\t\t\"category_id\",\n\t\t\tcategories.map((category) => category.category_id),\n\t\t);\n\t}\n\n\tconst { data: baseItems, error: baseItemsError } = await baseItemsQuery;\n\n\tif (baseItemsError) {\n\t\tthrow baseItemsError;\n\t}\n\n\tif (!baseItems || baseItems.length === 0) {\n\t\treturn [];\n\t}\n\n\tconst { data: images, error: imagesError } = await supabase\n\t\t.from(\"base_item_images\")\n\t\t.select(\"*\")\n\t\t.in(\n\t\t\t\"base_item_id\",\n\t\t\tbaseItems.map((baseItem) => baseItem.base_item_id),\n\t\t)\n\t\t.order(\"sort_index\", { ascending: true })\n\t\t.order(\"image_id\", { ascending: true });\n\n\tif (imagesError) {\n\t\tthrow imagesError;\n\t}\n\n\treturn attachImages(baseItems, images ?? []);\n}\n\n/**\n * Group images onto their base items. `images` must already be in the order\n * callers should see them (sort_index, then image_id) — grouping preserves it,\n * so every render site can take `images[0]` as the primary image.\n */\nexport function attachImages<T extends { base_item_id: number }>(\n\tbaseItems: T[],\n\timages: BaseItemImage[],\n): Array<T & { images: BaseItemImage[] }> {\n\tconst byBaseItemId = new Map<number, BaseItemImage[]>();\n\tfor (const image of images) {\n\t\tconst group = byBaseItemId.get(image.base_item_id);\n\t\tif (group) group.push(image);\n\t\telse byBaseItemId.set(image.base_item_id, [image]);\n\t}\n\n\treturn baseItems.map((baseItem) => ({\n\t\t...baseItem,\n\t\timages: byBaseItemId.get(baseItem.base_item_id) ?? [],\n\t}));\n}\n\nexport function menuImageUrl(\n\timage?: BaseItemImage | null,\n\tabsolute = false,\n): string | null {\n\tif (!image) return null;\n\tif (image.local_key && !absolute) return `/images/items/${image.local_key}`;\n\tconst path = image.processed_path ?? image.raw_path;\n\tif (!path) return null;\n\treturn `${SUPABASE_URL}/storage/v1/object/public/product-images/${path}`;\n}\n\nasync function getItems(\n\tsupabase: SupabaseClient,\n\tbaseItems: BaseItem[],\n\texcludedIds?: number[],\n): Promise<ItemVariant[]> {\n\tif (baseItems && baseItems.length === 0) {\n\t\treturn [];\n\t}\n\n\t// the scheme rides along so the prebuilt menu carries it — no runtime fetch\n\t// on the item page just to know how a pizza can be cut\n\tlet itemsQuery = supabase\n\t\t.from(\"items\")\n\t\t.select(\"*, portion_scheme:portion_schemes(*)\")\n\t\t.in(\n\t\t\t\"base_item_id\",\n\t\t\tbaseItems.map((baseItem) => baseItem.base_item_id),\n\t\t);\n\n\tif (excludedIds && excludedIds.length > 0) {\n\t\titemsQuery = itemsQuery.not(\"item_id\", \"in\", csvFormat(excludedIds));\n\t}\n\n\tconst { data: items, error: itemsError } = await itemsQuery;\n\n\tif (itemsError) {\n\t\tthrow itemsError;\n\t}\n\n\treturn items ?? [];\n}\n\nexport async function getComponents(\n\tsupabase: SupabaseClient,\n\titems: ItemVariant[] | number[],\n\tuseBehaviors = false,\n): Promise<Component[]> {\n\tconst itemIds = items.length > 0 && typeof items[0] === \"number\"\n\t\t? (items as number[])\n\t\t: (items as ItemVariant[]).map((item) => item.item_id);\n\n\tif (itemIds.length === 0) {\n\t\treturn [];\n\t}\n\n\tconst select = useBehaviors\n\t\t? \"components(*), item_id, servings_per_item, behavior_id, component_quantity_behaviors(headcount_threshold, starting_amount, step_size, step_interval, maximum)\"\n\t\t: \"components(*), item_id, servings_per_item\";\n\n\tconst { data: components } = await supabase\n\t\t.from(\"item_components\")\n\t\t.select(select)\n\t\t.in(\"item_id\", itemIds)\n\t\t.throwOnError();\n\n\treturn components.map((component: any) => ({\n\t\tservings_per_item: component.servings_per_item,\n\t\tis_base: component.components?.is_base,\n\t\tserving_weight: component.components?.serving_weight,\n\t\tcomponent_type_id: component.components?.component_type_id,\n\t\tid: component.components?.id,\n\t\tname: component.components?.name,\n\t\titem_id: component.item_id,\n\t\t...(useBehaviors && { behavior: component.component_quantity_behaviors ?? undefined }),\n\t}));\n}\n\n/**\n * A rule's options — components and items — as one keyed list. The key is what\n * `portionKey` builds on and what `weightedUpcharges` prices, so the picker and\n * the server's repricing read option prices through this same function.\n */\nexport function ruleOptionDefs(rule: Rule) {\n\treturn [\n\t\t...(rule.components ?? []).map((c) => ({\n\t\t\tkey: `component:${c.component_id}`,\n\t\t\tid: c.component_id,\n\t\t\ttype: \"component\" as const,\n\t\t\tname: c.name,\n\t\t\tbasePrice: c.upcharge_price || 0,\n\t\t\tdescription: c.description,\n\t\t})),\n\t\t...(rule.items ?? []).map((i) => ({\n\t\t\tkey: `item:${i.item_id}`,\n\t\t\tid: i.item_id,\n\t\t\ttype: \"item\" as const,\n\t\t\tname: i.name,\n\t\t\tbasePrice: i.upcharge_price || 0,\n\t\t\tdescription: i.description,\n\t\t})),\n\t];\n}\n\n/**\n * Rules attach at item, base-item, or category level; `ruleIds` fetches them by\n * primary key instead, for when you already hold the ids (repricing an order's\n * selections server-side).\n */\nexport async function getRules(\n\tsupabase: SupabaseClient,\n\tbaseItems?: BaseItem[],\n\tcategories?: Category[],\n\titems?: ItemVariant[],\n\tuseBehaviors = false,\n\truleIds?: number[],\n): Promise<Rule[]> {\n\tconst itemIds = items?.map((item) => item.item_id) ?? [];\n\tconst baseItemIds = baseItems?.map((baseItem) => baseItem.base_item_id) ?? [];\n\tconst categoryIds = categories?.map((category) => category.category_id) ?? [];\n\n\tif (\n\t\titemIds.length === 0 &&\n\t\tbaseItemIds.length === 0 &&\n\t\tcategoryIds.length === 0 &&\n\t\t!ruleIds?.length\n\t) {\n\t\treturn [];\n\t}\n\n\tconst orFilter = [\n\t\t`item_id.in.(${itemIds.length > 0 ? itemIds.join(\",\") : -1})`,\n\t\t`base_item_id.in.(${baseItemIds.length > 0 ? baseItemIds.join(\",\") : -1})`,\n\t\t`category_id.in.(${categoryIds.length > 0 ? categoryIds.join(\",\") : -1})`,\n\t\t...(ruleIds?.length ? [`rule_id.in.(${ruleIds.join(\",\")})`] : []),\n\t].join(\",\");\n\n\tconst behaviorSelect = useBehaviors\n\t\t? \"component_quantity_behaviors(headcount_threshold, starting_amount, step_size, step_interval, maximum)\"\n\t\t: null;\n\n\tconst ruleComponentsSelect = [\n\t\t\"component_id, upcharge_price, components(name, description, serving_weight)\",\n\t\tbehaviorSelect,\n\t].filter(Boolean).join(\", \");\n\n\tconst ruleItemsSelect = [\n\t\t\"item_id, upcharge_price, items(item_id, name, description)\",\n\t\tbehaviorSelect,\n\t].filter(Boolean).join(\", \");\n\n\tconst { data: ruleData, error: rulesError } = await supabase\n\t\t.from(\"item_rules\")\n\t\t.select(\n\t\t\t`\n rule_id,\n category_id,\n item_id,\n base_item_id,\n group_name,\n min_selection,\n max_selection,\n included_quantity,\n is_portionable,\n rule_components (${ruleComponentsSelect}),\n rule_items (${ruleItemsSelect})\n `,\n\t\t)\n\t\t.or(orFilter);\n\n\tif (rulesError) {\n\t\tthrow rulesError;\n\t}\n\n\treturn (ruleData ?? []).map((rule: any) => ({\n\t\trule_id: rule.rule_id,\n\t\tgroup_name: rule.group_name,\n\t\tmin_selection: rule.min_selection,\n\t\tmax_selection: rule.max_selection,\n\t\tincluded_quantity: rule.included_quantity,\n\t\tis_portionable: rule.is_portionable,\n\t\tcategory_id: rule.category_id,\n\t\titem_id: rule.item_id,\n\t\tbase_item_id: rule.base_item_id,\n\n\t\tcomponents: (rule.rule_components ?? []).map((component: any) => ({\n\t\t\tcomponent_id: component.component_id,\n\t\t\tupcharge_price: component.upcharge_price ?? 0,\n\t\t\tname: component.components?.name ?? \"\",\n\t\t\tdescription: component.components?.description ?? undefined,\n\t\t\tserving_weight: component.components?.serving_weight ?? undefined,\n\t\t\t...(useBehaviors && { behavior: component.component_quantity_behaviors ?? undefined }),\n\t\t})),\n\n\t\titems: (rule.rule_items ?? []).map((item: any) => ({\n\t\t\titem_id: item.item_id,\n\t\t\tname: item.items?.name ?? \"\",\n\t\t\tdescription: item.items?.description ?? undefined,\n\t\t\tupcharge_price: item.upcharge_price ?? 0,\n\t\t\t...(useBehaviors && { behavior: item.component_quantity_behaviors ?? undefined }),\n\t\t})),\n\t}));\n}\n\nfunction buildMenu(\n\titems: ItemVariant[],\n\tbaseItems: BaseItem[],\n\tcategories: Category[],\n\tcomponents: Component[],\n\trules: Rule[],\n): Menu {\n\tconst menu: Menu = {\n\t\titems: [],\n\t\tcategories: [],\n\t\trules,\n\t\tcomponents,\n\t};\n\n\tmenu.items = baseItems.map((baseItem) => {\n\t\tconst categoryRules = rules.filter((rule) => rule.category_id === baseItem.category_id);\n\t\tconst baseItemRules = rules.filter((rule) => rule.base_item_id === baseItem.base_item_id);\n\t\treturn {\n\t\t\tis_catering: (baseItem as any).per_person !== null,\n\t\t\t...baseItem,\n\t\t\tvariants: items\n\t\t\t\t.filter((item) => item.base_item_id === baseItem.base_item_id)\n\t\t\t\t.map((item) => ({\n\t\t\t\t\t...item,\n\t\t\t\t\tcomponents: components.filter(\n\t\t\t\t\t\t(component) => component.item_id === item.item_id,\n\t\t\t\t\t),\n\t\t\t\t\trules: [\n\t\t\t\t\t\t...categoryRules,\n\t\t\t\t\t\t...baseItemRules,\n\t\t\t\t\t\t...rules.filter((rule) => rule.item_id === item.item_id),\n\t\t\t\t\t],\n\t\t\t\t})),\n\t\t\trules: baseItemRules,\n\t\t};\n\t});\n\n\tmenu.categories = categories.map((category) => ({\n\t\t...category,\n\t\trules: rules.filter((rule) => rule.category_id === category.category_id),\n\t}));\n\n\treturn menu;\n}\n\nexport function getBaseItemFromVariant(\n\tmenu: MenuItem[],\n\tvariant: ItemVariant,\n\tbaseItemId?: number,\n): MenuItem | undefined {\n\tif (baseItemId != null) {\n\t\treturn menu.find((i) => i.base_item_id === baseItemId);\n\t}\n\treturn menu.find((i) => i.variants.some((v) => v.item_id === variant.item_id));\n}\n\nexport function getMenuItemByVariantId(menu: MenuItem[], variantId: number): MenuItem | undefined {\n\treturn menu.find((item) =>\n\t\titem.variants?.some((v) => v.item_id === variantId)\n\t);\n}\n\nexport function filterBySchedule(\n\tpromos: Promotion[],\n\tplatform: Platforms,\n\tlocationId: number | undefined,\n\tnow: Date = new Date(),\n): Promotion[] {\n\treturn promos.filter((promo) => {\n\t\tif (promo.start_date && new Date(promo.start_date) > now) return false;\n\t\tif (promo.end_date && new Date(promo.end_date) < now) return false;\n\n\t\tif (promo.platforms && promo.platforms.length > 0) {\n\t\t\tif (!promo.platforms.includes(platform)) return false;\n\t\t}\n\n\t\tif (!promo.is_automatic) return false;\n\n\t\tif (promo.location_id != null && locationId !== undefined) {\n\t\t\tif (promo.location_id !== locationId) return false;\n\t\t} else if (promo.location_id != null && locationId === undefined) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif (promo.schedule_details_json) {\n\t\t\tconst { interval, days, start_time, end_time } =\n\t\t\t\tpromo.schedule_details_json;\n\n\t\t\tif (interval === \"weekly\" && days && !days.includes(getDay(now) + 1)) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst dayStart = startOfDay(now);\n\t\t\tif (start_time && isBefore(now, parse(start_time, \"HH:mm\", dayStart))) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (end_time && isAfter(now, parse(end_time, \"HH:mm\", dayStart))) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t});\n}\n\nexport async function getActivePromotions(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n\tplatform: Platforms,\n\tlocationId?: number,\n): Promise<Promotion[]> {\n\tconst { data, error } = await supabase\n\t\t.from(\"promotions\")\n\t\t.select(\"*\")\n\t\t.eq(\"is_active\", true)\n\t\t.eq(\"franchise_id\", franchiseId);\n\n\tif (error) throw error;\n\n\treturn filterBySchedule((data || []) as Promotion[], platform, locationId);\n}\n","export { menuImageUrl } from \"@zorgo/universal/utils/menuUtils\";\nexport type { BaseItemImage } from \"@zorgo/universal/interfaces\";\n\n/**\n * The variant widths prebuild emits. The loader below and the encoder in\n * internal/itemImages.ts read the same list, so a requested width can never\n * snap to a rung that was never written.\n */\nexport const ITEM_IMAGE_WIDTHS = [384, 640, 1080] as const;\n\n/** Public prefix menuImageUrl returns for a localized image. */\nexport const ITEM_IMAGE_PREFIX = \"/images/items/\";\n\n/**\n * next/image custom loader (`images.loaderFile` in a scaffolded site's\n * next.config.ts). Prebuilt images get the nearest rung that covers the\n * requested width; everything else — a bucket URL for an image prebuild could\n * not localize, the logo, any site-authored asset — passes through untouched\n * and is simply served unoptimized.\n *\n * Sync and pure by contract: next/image calls it during render for every\n * srcset candidate.\n */\nexport default function itemImageLoader({\n\tsrc,\n\twidth,\n}: {\n\tsrc: string;\n\twidth: number;\n}): string {\n\tif (!src.startsWith(ITEM_IMAGE_PREFIX)) return src;\n\tconst rung =\n\t\tITEM_IMAGE_WIDTHS.find((candidate) => candidate >= width) ??\n\t\tITEM_IMAGE_WIDTHS[ITEM_IMAGE_WIDTHS.length - 1];\n\treturn `${src}-${rung}.avif`;\n}\n\nexport { itemImageLoader };\n"],"mappings":"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","names":["import_fs","import_path","menu","path","path","menu","path","menu"]}
1
+ {"version":3,"sources":["../src/server.ts","../../../node_modules/date-fns-tz/dist/esm/_lib/tzTokenizeDate/index.js","../../../node_modules/date-fns-tz/dist/esm/format/formatters/index.js","../../zorgo/universal/utils/locationOrderingAvailability.ts","../src/seo.ts","../src/internal/images.ts","../../zorgo/universal/constants.ts","../../zorgo/universal/utils/menuUtils.ts","../src/images.ts"],"sourcesContent":["// Server-only entry (@zorgo/next/server). Node/Server-Component safe: no React,\n// no \"use client\", no client hooks — so scripts/prebuild.ts and the\n// app/api/zorgo-token route can import the token-exchange and build pipeline\n// from the package instead of scaffolding their own copies. Shipping these here\n// means a package update fixes every site, rather than each site freezing a\n// forked copy at scaffold time.\nimport { writeFileSync, mkdirSync, existsSync, readdirSync } from \"fs\";\nimport path from \"path\";\nimport { loadEnvConfig } from \"@next/env\";\nimport { z } from \"zod\";\nimport type { Menu } from \"@zorgo/universal/utils/menuUtils\";\nimport type { Location } from \"@zorgo/universal/interfaces\";\nimport { buildRestaurantJsonLd } from \"./seo\";\nimport { optimizeSiteImages, stampItemImages } from \"./internal/images\";\n\n// localhost fallback keeps a stray dev run from silently hitting production.\n// Read at call time, never at module load: prebuild imports this file before it\n// calls loadEnvConfig(), and ES imports are evaluated first — a module-level\n// read would freeze to the fallback before .env is loaded.\nfunction apiBaseUrl(): string {\n\treturn process.env.NEXT_PUBLIC_API_BASE_URL ?? \"http://localhost:3000\";\n}\n\n/** Why a site token could not be obtained — lets callers report precisely. */\nexport type SiteTokenFailure = \"missing-secret\" | \"rejected\" | \"unreachable\";\n\nexport class SiteTokenError extends Error {\n\treadonly reason: SiteTokenFailure;\n\tconstructor(reason: SiteTokenFailure, message: string, options?: { cause?: unknown }) {\n\t\tsuper(message, options);\n\t\tthis.name = \"SiteTokenError\";\n\t\tthis.reason = reason;\n\t}\n}\n\n/**\n * Exchange ZORGO_SITE_SECRET for a short-lived site token.\n *\n * Throws a {@link SiteTokenError} that distinguishes the three failure modes\n * (secret not set / endpoint rejected the secret / endpoint unreachable) rather\n * than collapsing them into one misleading \"missing secret\" message.\n */\nexport async function fetchSiteToken(): Promise<string> {\n\tconst secret = process.env.ZORGO_SITE_SECRET;\n\tif (!secret) {\n\t\tthrow new SiteTokenError(\n\t\t\t\"missing-secret\",\n\t\t\t\"ZORGO_SITE_SECRET is not set. Add it to this site's .env.\",\n\t\t);\n\t}\n\n\tconst url = `${apiBaseUrl()}/auth/site-token`;\n\tlet res: Response;\n\ttry {\n\t\tres = await fetch(url, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: { \"x-zorgo-site-secret\": secret },\n\t\t});\n\t} catch (cause) {\n\t\tthrow new SiteTokenError(\n\t\t\t\"unreachable\",\n\t\t\t`Could not reach the site-token endpoint at ${url}. Check NEXT_PUBLIC_API_BASE_URL and that the API is running.`,\n\t\t\t{ cause },\n\t\t);\n\t}\n\n\tif (!res.ok) {\n\t\tconst body = (await res.text().catch(() => \"\")).slice(0, 200);\n\t\tthrow new SiteTokenError(\n\t\t\t\"rejected\",\n\t\t\t`The site-token endpoint rejected the request (${res.status} ${res.statusText}) at ${url}. ` +\n\t\t\t\t`This usually means ZORGO_SITE_SECRET is wrong for this environment.${body ? ` Response: ${body}` : \"\"}`,\n\t\t);\n\t}\n\n\tconst { token } = (await res.json()) as { token?: string };\n\tif (!token) {\n\t\tthrow new SiteTokenError(\n\t\t\t\"rejected\",\n\t\t\t`The site-token endpoint returned 200 but no token at ${url}.`,\n\t\t);\n\t}\n\treturn token;\n}\n\n// ---------------------------------------------------------------------------\n// Prebuild pipeline\n// ---------------------------------------------------------------------------\n// Everything below runs at build time in a scaffolded site's cwd, fetching\n// franchise data and writing the generated modules/assets the site imports.\n\ninterface FetchArgs {\n\tfranchiseId: number;\n\ttoken: string;\n}\n\nconst BrandingSchema = z\n\t.object({\n\t\tbackgroundBase: z.string().default(\"#F5F5F5\"),\n\t\tprimary: z.string().default(\"#3264FF\"),\n\t\taccent: z.string().default(\"#9696FF\"),\n\t\tfontFamily: z.string().default(\"Inter\"), // Google Font family name\n\t})\n\t.prefault({});\ntype Branding = z.infer<typeof BrandingSchema>;\n\n// A freshly-created franchise commonly has null columns (no site_url set yet,\n// name not filled in). Coerce null|undefined to sensible values instead of\n// letting z.string() throw on null and blow up JSON-LD generation.\nconst FranchiseSchema = z\n\t.object({\n\t\tname: z\n\t\t\t.string()\n\t\t\t.nullish()\n\t\t\t.transform((v) => v ?? process.env.ZORGO_PROJECT_NAME ?? \"Restaurant\"),\n\t\tsite_url: z\n\t\t\t.string()\n\t\t\t.nullish()\n\t\t\t.transform((v) => v ?? undefined),\n\t})\n\t.prefault({});\n\nconst LocationsSchema = z.array(z.unknown()).prefault([]);\n\n// Mirrors PublicFeatureConfig on main_service. Every branch is nullable and the\n// whole thing prefaults, so a franchise with no config rows — or an API that\n// 404s because it hasn't been deployed yet — writes a file of nulls instead of\n// breaking the build.\nconst FeatureConfigSchema = z\n\t.object({\n\t\tcatering: z\n\t\t\t.object({\n\t\t\t\tdoes_cater: z.boolean().nullish(),\n\t\t\t\tminimum_notice_hours: z.number().nullish(),\n\t\t\t\tminimum_people_required: z.number().nullish(),\n\t\t\t\tminimum_price: z.number().nullish(),\n\t\t\t\tpreferred_time: z.string().nullish(),\n\t\t\t})\n\t\t\t.nullish()\n\t\t\t.transform((v) => v ?? null),\n\t\tcatering_bulk_discount: z\n\t\t\t.object({\n\t\t\t\tis_active: z.boolean().nullish(),\n\t\t\t\tfull_discount_at: z.number().nullish(),\n\t\t\t\tmaximum_percent_off: z.number().nullish(),\n\t\t\t})\n\t\t\t.nullish()\n\t\t\t.transform((v) => v ?? null),\n\t\t// Passed through unvalidated: the server already parsed it against\n\t\t// FoodTruckConfig, and re-declaring eight money fields here just creates a\n\t\t// second place to forget one. The site imports it as FoodTruckConfigValue.\n\t\tfood_truck: z.unknown().transform((v) => v ?? null),\n\t})\n\t.prefault({ food_truck: null });\n\nconst MenuSchema = z.object({\n\titems: z.array(z.unknown()).default([]),\n\tcategories: z.array(z.unknown()).default([]),\n\tcomponents: z.array(z.unknown()).default([]),\n\trules: z.array(z.unknown()).default([]),\n});\n\nasync function fetchData<T>(\n\tendpoint: string,\n\ttoken: string,\n\tschema: z.ZodType<T>,\n): Promise<T> {\n\ttry {\n\t\tconst res = await fetch(`${apiBaseUrl()}${endpoint}`, {\n\t\t\theaders: { Authorization: `Bearer ${token}` },\n\t\t});\n\t\tif (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);\n\t\tconst body = await res.json();\n\t\treturn schema.parse(body?.data ?? body);\n\t} catch (err) {\n\t\tconsole.error(`prebuild: ${endpoint} failed, using defaults:`, err);\n\t\treturn schema.parse(undefined);\n\t}\n}\n\n// style stuff\nconst FONT_ON_LIGHT = \"#0a0a0a\";\nconst FONT_ON_DARK = \"#f5f5f5\";\nconst NEUTRAL_GRAY = { r: 128, g: 128, b: 128 };\n/** How far a surface shifts toward neutral gray. Larger reads as more separated from the base. */\nconst SURFACE_SHIFT_SUBTLE = 0.1;\nconst SURFACE_SHIFT_STRONG = 0.18;\n\nfunction parseHexRgb(background: string): { r: number; g: number; b: number } | null {\n\tconst hex = background.trim().replace(/^#/, \"\");\n\tconst full =\n\t\thex.length === 3\n\t\t\t? hex\n\t\t\t\t\t.split(\"\")\n\t\t\t\t\t.map((c) => c + c)\n\t\t\t\t\t.join(\"\")\n\t\t\t: hex;\n\tif (full.length !== 6 || Number.isNaN(Number.parseInt(full, 16))) {\n\t\treturn null;\n\t}\n\treturn {\n\t\tr: Number.parseInt(full.slice(0, 2), 16),\n\t\tg: Number.parseInt(full.slice(2, 4), 16),\n\t\tb: Number.parseInt(full.slice(4, 6), 16),\n\t};\n}\n\nfunction toHex({ r, g, b }: { r: number; g: number; b: number }): string {\n\treturn `#${[r, g, b].map((c) => c.toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n\nfunction relativeLuminance({ r, g, b }: { r: number; g: number; b: number }): number {\n\treturn (0.299 * r + 0.587 * g + 0.114 * b) / 255;\n}\n\n/** Off-black on light backgrounds, off-white on dark. */\nfunction deriveFontColor(background: string): string {\n\tconst rgb = parseHexRgb(background);\n\tif (!rgb) return FONT_ON_LIGHT;\n\treturn relativeLuminance(rgb) > 0.5 ? FONT_ON_LIGHT : FONT_ON_DARK;\n}\n\nfunction lerpToward(\n\trgb: { r: number; g: number; b: number },\n\ttarget: { r: number; g: number; b: number },\n\tt: number,\n): string {\n\treturn toHex({\n\t\tr: Math.round(rgb.r + (target.r - rgb.r) * t),\n\t\tg: Math.round(rgb.g + (target.g - rgb.g) * t),\n\t\tb: Math.round(rgb.b + (target.b - rgb.b) * t),\n\t});\n}\n\n/**\n * A raised surface (cards, panels) reads as lighter on a dark base and grayer\n * on a light base — nudging toward neutral gray gets both for free. Unlike the\n * old flat +delta-per-channel formula, a lerp can never clip: a white base\n * can't get any lighter, so `min(255, 255 + delta)` produced an invisible card\n * on a white background (e.g. Zestia's #FFFFFF, whose Modish spec wants a\n * visibly grayer #F1F1F2 card).\n */\nfunction deriveElevatedBackground(background: string): string {\n\tconst rgb = parseHexRgb(background);\n\tif (!rgb) return background;\n\tconst t = relativeLuminance(rgb) > 0.5 ? SURFACE_SHIFT_SUBTLE : SURFACE_SHIFT_STRONG;\n\treturn lerpToward(rgb, NEUTRAL_GRAY, t);\n}\n\n/** Inset surface (wells, sunken fields) — shifts toward gray less than elevated on a dark base, more on a light one, so it always reads as the deeper of the two. */\nfunction deriveRecessedBackground(background: string): string {\n\tconst rgb = parseHexRgb(background);\n\tif (!rgb) return background;\n\tconst t = relativeLuminance(rgb) > 0.5 ? SURFACE_SHIFT_STRONG : SURFACE_SHIFT_SUBTLE;\n\treturn lerpToward(rgb, NEUTRAL_GRAY, t);\n}\n\n// Omit empty values: opennextjs-cloudflare injects [vars] into the build env, so\n// an empty NEXT_PUBLIC_SITE_URL (a site piloted before its URL is set) would land\n// as \"\" — a defined-but-empty value that slips past the `?? localhost` fallbacks in\n// layout/robots/sitemap and makes new URL(\"\") throw, failing the build/deploy.\n// Leaving the var out keeps it `undefined` so those fallbacks fire.\nfunction varsBlock(vars: Record<string, string>): string {\n\treturn Object.entries(vars)\n\t\t.filter(([, value]) => value !== \"\")\n\t\t.map(([key, value]) => `${key} = \"${value}\"`)\n\t\t.join(\"\\n\");\n}\n\n/**\n * Pure builder, kept separate from the file write so it's testable without\n * touching the filesystem or real process.env. `env` is the site's .env — the\n * single source of truth wrangler.toml is regenerated from on every prebuild.\n */\nexport function buildWranglerToml(env: NodeJS.ProcessEnv): string {\n\tconst name = env.ZORGO_PROJECT_NAME ?? \"zorgo-site\";\n\tconst siteUrl = env.NEXT_PUBLIC_SITE_URL ?? \"\";\n\tconst apiUrl = env.NEXT_PUBLIC_API_BASE_URL ?? \"\";\n\tconst baseUrl = env.NEXT_PUBLIC_BASE_URL ?? siteUrl;\n\tconst supabaseUrl = env.NEXT_PUBLIC_SUPABASE_URL ?? \"\";\n\n\t// custom_domain routes need a bare hostname; skip them if the site URL isn't\n\t// a parseable absolute URL (e.g. a local/placeholder value).\n\tlet host: string | undefined;\n\ttry {\n\t\thost = new URL(siteUrl).hostname;\n\t} catch {\n\t\thost = undefined;\n\t}\n\tconst routes = host\n\t\t? `\n[[routes]]\npattern = \"${host}\"\ncustom_domain = true\n\n[[routes]]\npattern = \"www.${host}\"\ncustom_domain = true\n`\n\t\t: \"\";\n\n\t// Zestia's hand-written toml (the target shape for the port) runs\n\t// nodejs_compat_v2, not the nodejs_compat this used to emit unconditionally.\n\tconst toml = `# AUTO-GENERATED by scripts/prebuild.ts from .env — do not edit.\nname = \"${name}\"\nmain = \".open-next/worker.js\"\ncompatibility_date = \"2024-09-23\"\ncompatibility_flags = [\"nodejs_compat_v2\"]\n\n[assets]\ndirectory = \".open-next/assets\"\nbinding = \"ASSETS\"\n\n[vars]\n${varsBlock({\n\tNEXT_PUBLIC_SUPABASE_URL: supabaseUrl,\n\tNEXT_PUBLIC_API_BASE_URL: apiUrl,\n\tNEXT_PUBLIC_SITE_URL: siteUrl,\n\tNEXT_PUBLIC_BASE_URL: baseUrl,\n})}\n\n[observability]\nenabled = true\n\n[observability.logs]\nenabled = true\ninvocation_logs = true\n${routes}${buildStagingBlock(env, name, supabaseUrl, apiUrl)}`;\n\n\treturn toml;\n}\n\n/**\n * A staging worker is what lets a ported site ship gradually without touching\n * the production custom domain — so `routes = []` here is load-bearing, not\n * optional. Driven by one env var (NEXT_PUBLIC_STAGING_BASE_URL) rather than a\n * general multi-env list: today's ports only ever need the one staging env.\n */\nfunction buildStagingBlock(\n\tenv: NodeJS.ProcessEnv,\n\tname: string,\n\tsupabaseUrl: string,\n\tapiUrl: string,\n): string {\n\tconst stagingUrl = env.NEXT_PUBLIC_STAGING_BASE_URL ?? \"\";\n\treturn `\n[env.staging]\nname = \"${name}-staging\"\nroutes = []\n\n[env.staging.vars]\n${varsBlock({\n\tNEXT_PUBLIC_SUPABASE_URL: supabaseUrl,\n\tNEXT_PUBLIC_API_BASE_URL: apiUrl,\n\tNEXT_PUBLIC_SITE_URL: stagingUrl,\n\tNEXT_PUBLIC_BASE_URL: stagingUrl,\n})}\n`;\n}\n\nfunction writeWranglerToml() {\n\twriteFileSync(path.join(process.cwd(), \"wrangler.toml\"), buildWranglerToml(process.env));\n}\n\nasync function logo({ franchiseId, token }: FetchArgs) {\n\tconst logoResponse = await fetch(\n\t\t`${apiBaseUrl()}/franchise-info/logo/${franchiseId}`,\n\t\t{ headers: { Authorization: `Bearer ${token}` } },\n\t);\n\n\t// 404 (no logo) returns a JSON body — don't persist it as logo.png, or the\n\t// icon/OG image would render an error blob instead of falling back.\n\tif (!logoResponse.ok) return;\n\n\tconst arrayBuffer = await logoResponse.arrayBuffer();\n\tconst buffer = Buffer.from(arrayBuffer);\n\n\tconst outDir = path.join(process.cwd(), `public/images/`);\n\tmkdirSync(outDir, { recursive: true });\n\t// One file regardless of the source extension: fetchLogo (main_service) probes\n\t// storage for logo.png then logo.jpg and returns whichever exists, so by here\n\t// there is exactly one blob to persist.\n\twriteFileSync(path.join(outDir, \"logo.png\"), buffer);\n}\n\n// Embedding vectors are per-item and huge; they'd ship in the client bundle for\n// nothing. ponytail: name-based drop, revisit if the API starts returning the\n// menu trimmed already.\nconst dropHeavyColumns = (key: string, value: unknown) =>\n\tkey.endsWith(\"_vector\") || key.endsWith(\"_embedding\") ? undefined : value;\n\nfunction writeMenuModule(menu: Menu) {\n\tconst outDir = path.join(process.cwd(), `lib/generated/`);\n\tmkdirSync(outDir, { recursive: true });\n\t// typed module so consumers get Menu-checked imports instead of an\n\t// untyped runtime fetch of menu.json.\n\t//\n\t// The API returns whole DB rows, which carry columns the interfaces don't\n\t// declare (created_at, status_id, embeddings...). Annotating the literal\n\t// directly triggers TS excess-property checking and fails the site build\n\t// every time a column is added, so assert instead — the shape is validated\n\t// server-side, and consumers still import a typed `Menu`.\n\twriteFileSync(\n\t\tpath.join(outDir, \"menu.ts\"),\n\t\t`// AUTO-GENERATED by scripts/prebuild.ts — do not edit.\\n` +\n\t\t\t`import type { Menu } from \"@zorgo/next\";\\n\\n` +\n\t\t\t`export const menu = ${JSON.stringify(menu, dropHeavyColumns, 2)} as unknown as Menu;\\n`,\n\t);\n}\n\nasync function menu({ franchiseId, token }: FetchArgs): Promise<Menu> {\n\tconst menuResponse = await fetch(\n\t\t`${apiBaseUrl()}/franchise-info/menu/${franchiseId}`,\n\t\t{ headers: { Authorization: `Bearer ${token}` } },\n\t);\n\n\t// A non-ok response is a real failure (auth, server error) — fail loudly.\n\tif (!menuResponse.ok) {\n\t\tthrow new Error(\n\t\t\t`menu fetch failed ${menuResponse.status}: ${await menuResponse.text()}`,\n\t\t);\n\t}\n\n\tconst body = await menuResponse.json();\n\tconst parsed = MenuSchema.parse(body?.data ?? {}) as Menu;\n\n\t// An empty menu is expected for a freshly-initialized franchise that hasn't\n\t// added items yet. Ship a valid empty menu.ts and keep building — the\n\t// storefront renders its empty state instead of failing the whole build.\n\tif (parsed.items.length === 0) {\n\t\tconsole.warn(\n\t\t\t\"prebuild: franchise has no menu items yet — generating an empty menu. \" +\n\t\t\t\t\"Add items in the dashboard and rebuild to populate the storefront.\",\n\t\t);\n\t}\n\n\t// Must precede writeMenuModule: it stamps local_key onto the image records\n\t// that get serialized into menu.ts.\n\tawait stampItemImages(parsed, process.cwd());\n\n\twriteMenuModule(parsed);\n\treturn parsed;\n}\n\n// franchise row (name, site_url) -> Restaurant JSON-LD, written as a typed\n// module the layout imports and drops into <head>.\nasync function jsonLd(\n\t{ franchiseId, token }: FetchArgs,\n\tmenuData: Menu,\n\tlocationRows: Location[],\n) {\n\tconst franchise = await fetchData(\n\t\t`/franchise-info/franchise/${franchiseId}`,\n\t\ttoken,\n\t\tFranchiseSchema,\n\t);\n\tconst url = franchise.site_url;\n\n\tconst data = buildRestaurantJsonLd(menuData, {\n\t\tname: franchise.name,\n\t\turl,\n\t\timage: url ? `${url}/images/logo.png` : undefined,\n\t\tlocations: locationRows,\n\t});\n\n\tconst outDir = path.join(process.cwd(), `lib/generated/`);\n\tmkdirSync(outDir, { recursive: true });\n\twriteFileSync(\n\t\tpath.join(outDir, \"jsonld.ts\"),\n\t\t`// AUTO-GENERATED by scripts/prebuild.ts — do not edit.\\n` +\n\t\t\t`export const restaurantJsonLd = ${JSON.stringify(data, null, 2)};\\n`,\n\t);\n}\n\n// public locations (address, coordinates, weekly hours) -> a typed module the\n// site imports, so hours render into the static HTML instead of arriving after\n// the client store hydrates.\nasync function locations({ franchiseId, token }: FetchArgs): Promise<Location[]> {\n\tconst rows = await fetchData(\n\t\t`/franchise-info/locations/${franchiseId}`,\n\t\ttoken,\n\t\tLocationsSchema,\n\t);\n\n\tconst outDir = path.join(process.cwd(), `lib/generated/`);\n\tmkdirSync(outDir, { recursive: true });\n\t// Asserted rather than annotated for the same reason menu.ts is: the API\n\t// returns whole DB rows, and excess-property checking would fail the site\n\t// build every time a column is added.\n\twriteFileSync(\n\t\tpath.join(outDir, \"locations.ts\"),\n\t\t`// AUTO-GENERATED by scripts/prebuild.ts — do not edit.\\n` +\n\t\t\t`import type { Location } from \"@zorgo/next\";\\n\\n` +\n\t\t\t`export const locations = ${JSON.stringify(rows, null, 2)} as unknown as Location[];\\n`,\n\t);\n\treturn rows as Location[];\n}\n\n// Franchise settings a page reads but never mutates (catering minimums, the bulk\n// discount, food-truck pricing). Prebuilt rather than fetched at runtime: it's\n// build-time-constant per deploy, so a store or a hook for it would be three\n// moving parts to deliver a constant — and the client no longer needs an anon\n// Supabase read against franchise_feature_config to get it.\nasync function featureConfig({ franchiseId, token }: FetchArgs) {\n\tconst config = await fetchData(\n\t\t`/franchise-info/feature-config/${franchiseId}`,\n\t\ttoken,\n\t\tFeatureConfigSchema,\n\t);\n\n\tconst outDir = path.join(process.cwd(), `lib/generated/`);\n\tconst outFile = path.join(outDir, \"featureConfig.ts\");\n\n\t// An all-null result means either \"this franchise configures none of these\"\n\t// or \"the endpoint 404'd and fetchData fell back to defaults\" — and the two\n\t// are indistinguishable from here. Overwriting a good file with nulls in the\n\t// second case silently turns off catering minimums and food-truck pricing on\n\t// the next deploy, so keep what is on disk and say so. A fresh clone has no\n\t// file yet and still gets one, so the build compiles either way.\n\tconst empty =\n\t\t!config.catering && !config.catering_bulk_discount && !config.food_truck;\n\tif (empty && existsSync(outFile)) {\n\t\tconsole.warn(\n\t\t\t\"prebuild: feature-config came back empty — keeping the existing lib/generated/featureConfig.ts.\",\n\t\t);\n\t\treturn;\n\t}\n\n\tmkdirSync(outDir, { recursive: true });\n\twriteFileSync(\n\t\toutFile,\n\t\t`// AUTO-GENERATED by scripts/prebuild.ts — do not edit.\\n` +\n\t\t\t`import type {\\n\\tCateringSiteConfig,\\n\\tCateringBulkDiscount,\\n\\tFoodTruckConfigValue,\\n} from \"@zorgo/next\";\\n\\n` +\n\t\t\t`export const cateringConfig: CateringSiteConfig | null = ${JSON.stringify(config.catering, null, 2)};\\n\\n` +\n\t\t\t`export const cateringBulkDiscount: CateringBulkDiscount | null = ${JSON.stringify(config.catering_bulk_discount, null, 2)};\\n\\n` +\n\t\t\t`export const foodTruck = ${JSON.stringify(config.food_truck ?? null, null, 2)} as FoodTruckConfigValue | null;\\n`,\n\t);\n}\n\nconst FONT_FILE_RE = /\\.(woff2?|ttf|otf)$/i;\n\n/**\n * A site that ships its own licensed font files in public/fonts/ (e.g.\n * Zestia's Gotham SSm .woff set) supplies its own @font-face rules in its\n * hand-owned globals.css — the generated branding.css must not also try to\n * @import the family from Google Fonts, or it silently replaces the site's\n * entire type system on the next prebuild.\n */\nfunction hasSelfHostedFonts(cwd: string): boolean {\n\tconst fontsDir = path.join(cwd, \"public/fonts\");\n\treturn existsSync(fontsDir) && readdirSync(fontsDir).some((f) => FONT_FILE_RE.test(f));\n}\n\nasync function styles({ franchiseId, token }: FetchArgs) {\n\tconst brand = await fetchData(\n\t\t`/franchise-info/styles/${franchiseId}`,\n\t\ttoken,\n\t\tBrandingSchema,\n\t);\n\n\t// Same derived palette branding.css uses, persisted as JSON so the build-time\n\t// icon/OG/manifest routes stay on brand (they readFileSync this).\n\tconst branding = {\n\t\t...brand,\n\t\tbackgroundElevated: deriveElevatedBackground(brand.backgroundBase),\n\t\tbackgroundRecessed: deriveRecessedBackground(brand.backgroundBase),\n\t\tforeground: deriveFontColor(brand.backgroundBase),\n\t\tprimaryForeground: deriveFontColor(brand.primary),\n\t\taccentForeground: deriveFontColor(brand.accent),\n\t};\n\n\tconst stylesDir = path.join(process.cwd(), `public/styles/`);\n\tmkdirSync(stylesDir, { recursive: true });\n\twriteFileSync(\n\t\tpath.join(stylesDir, \"styles.json\"),\n\t\tJSON.stringify(branding, null, 2),\n\t);\n\n\t// app/globals.css is hand-owned (see ZOR-391) and imports this file — never\n\t// write globals.css itself here, or a site loses any custom vars/@font-face\n\t// it added on the next `pnpm dev`.\n\tconst generatedDir = path.join(process.cwd(), \"app\", \"generated\");\n\tmkdirSync(generatedDir, { recursive: true });\n\twriteFileSync(\n\t\tpath.join(generatedDir, \"branding.css\"),\n\t\tbuildBrandingCss(brand, { selfHostedFont: hasSelfHostedFonts(process.cwd()) }),\n\t);\n}\n\n/**\n * Pure builder for the generated branding stylesheet — separated from styles()\n * so it's testable without a network fetch or filesystem. The Google Fonts\n * @import must be the first statement in this file: app/globals.css imports it\n * before `@import \"tailwindcss\"`, and Tailwind's own @import resolution would\n * otherwise land the font import after non-import content, which browsers\n * silently ignore.\n */\nexport function buildBrandingCss(\n\tbranding: Branding,\n\topts: { selfHostedFont: boolean },\n): string {\n\tconst foreground = deriveFontColor(branding.backgroundBase);\n\tconst primaryForeground = deriveFontColor(branding.primary);\n\tconst accentForeground = deriveFontColor(branding.accent);\n\tconst backgroundElevated = deriveElevatedBackground(branding.backgroundBase);\n\tconst backgroundRecessed = deriveRecessedBackground(branding.backgroundBase);\n\t// Escaped for the quoted --font-sans declaration below, not the Google Fonts\n\t// URL param (which only ever needs the space -> + substitution).\n\tconst fontFamilyEscaped = branding.fontFamily.replace(/\"/g, '\\\\\"');\n\n\tconst fontImport = opts.selfHostedFont\n\t\t? \"\"\n\t\t: `@import url(\"https://fonts.googleapis.com/css2?family=${branding.fontFamily.replace(/ /g, \"+\")}:wght@400;700&display=swap\");\\n`;\n\n\treturn `${fontImport}:root {\n --background: ${branding.backgroundBase};\n --background-elevated: ${backgroundElevated};\n --background-recessed: ${backgroundRecessed};\n --foreground: ${foreground};\n --primary: ${branding.primary};\n --primary-foreground: ${primaryForeground};\n --accent: ${branding.accent};\n --accent-foreground: ${accentForeground};\n --font-sans: \"${fontFamilyEscaped}\", sans-serif;\n}\n\n@theme inline {\n --color-background: var(--background);\n --color-background-elevated: var(--background-elevated);\n --color-background-recessed: var(--background-recessed);\n --color-foreground: var(--foreground);\n --color-primary: var(--primary);\n --color-primary-foreground: var(--primary-foreground);\n --color-accent: var(--accent);\n --color-accent-foreground: var(--accent-foreground);\n --font-sans: var(--font-sans);\n}\n`;\n}\n\n/**\n * Full prebuild pipeline for a scaffolded site. Loads the site's .env, exchanges\n * the site secret for a token, then fetches franchise data and writes the\n * generated modules/assets (wrangler.toml, menu.ts, jsonld.ts, styles, logo).\n * Called from the site's thin scripts/prebuild.ts wrapper.\n */\nexport async function runPrebuild(): Promise<void> {\n\tconsole.log(\"running prebuild script...\");\n\n\t// Load .env here so the wrapper stays a one-liner and can't reintroduce the\n\t// import-order bug (env must be loaded before any env read below).\n\tloadEnvConfig(process.cwd());\n\n\t// regenerate deploy config from .env before any network work\n\twriteWranglerToml();\n\n\tconst token = await fetchSiteToken();\n\n\tconst encodedPayload = token.split(\".\")[1];\n\tif (!encodedPayload) {\n\t\tthrow new Error(\"site token is not a well-formed JWT (no payload segment)\");\n\t}\n\tconst payload = JSON.parse(Buffer.from(encodedPayload, \"base64url\").toString());\n\tconst franchiseId = payload.franchise_ids[0];\n\n\t// menu + franchise feed the JSON-LD; a menu *fetch* failure breaks the build\n\t// loudly, but an empty menu (new franchise) is tolerated inside menu().\n\tconst [menuData, locationRows] = await Promise.all([\n\t\tmenu({ franchiseId, token }),\n\t\tlocations({ franchiseId, token }).catch((err) => {\n\t\t\tconsole.error(\"locations failed:\", err);\n\t\t\treturn [] as Location[];\n\t\t}),\n\t\tlogo({ franchiseId, token }).catch((err) => console.error(\"logo failed:\", err)),\n\t\tstyles({ franchiseId, token }).catch((err) => console.error(\"styles failed:\", err)),\n\t\tfeatureConfig({ franchiseId, token }).catch((err) =>\n\t\t\tconsole.error(\"featureConfig failed:\", err),\n\t\t),\n\t\t// Purely local (assets/images -> public/images), so it needs no token and\n\t\t// rides along with the network work instead of adding to the wall clock.\n\t\toptimizeSiteImages(process.cwd()).catch((err) =>\n\t\t\tconsole.error(\"site images failed:\", err),\n\t\t),\n\t]);\n\n\tawait jsonLd({ franchiseId, token }, menuData, locationRows).catch((err) =>\n\t\tconsole.error(\"jsonLd failed:\", err),\n\t);\n\n\tconsole.log(\"prebuild script completed\");\n}\n","/**\n * Returns the [year, month, day, hour, minute, seconds] tokens of the provided\n * `date` as it will be rendered in the `timeZone`.\n */\nexport function tzTokenizeDate(date, timeZone) {\n const dtf = getDateTimeFormat(timeZone);\n return 'formatToParts' in dtf ? partsOffset(dtf, date) : hackyOffset(dtf, date);\n}\nconst typeToPos = {\n year: 0,\n month: 1,\n day: 2,\n hour: 3,\n minute: 4,\n second: 5,\n};\nfunction partsOffset(dtf, date) {\n try {\n const formatted = dtf.formatToParts(date);\n const filled = [];\n for (let i = 0; i < formatted.length; i++) {\n const pos = typeToPos[formatted[i].type];\n if (pos !== undefined) {\n filled[pos] = parseInt(formatted[i].value, 10);\n }\n }\n return filled;\n }\n catch (error) {\n if (error instanceof RangeError) {\n return [NaN];\n }\n throw error;\n }\n}\nfunction hackyOffset(dtf, date) {\n const formatted = dtf.format(date);\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const parsed = /(\\d+)\\/(\\d+)\\/(\\d+),? (\\d+):(\\d+):(\\d+)/.exec(formatted);\n // const [, fMonth, fDay, fYear, fHour, fMinute, fSecond] = parsed\n // return [fYear, fMonth, fDay, fHour, fMinute, fSecond]\n return [\n parseInt(parsed[3], 10),\n parseInt(parsed[1], 10),\n parseInt(parsed[2], 10),\n parseInt(parsed[4], 10),\n parseInt(parsed[5], 10),\n parseInt(parsed[6], 10),\n ];\n}\n// Get a cached Intl.DateTimeFormat instance for the IANA `timeZone`. This can be used\n// to get deterministic local date/time output according to the `en-US` locale which\n// can be used to extract local time parts as necessary.\nconst dtfCache = {};\n// New browsers use `hourCycle`, IE and Chrome <73 does not support it and uses `hour12`\nconst testDateFormatted = new Intl.DateTimeFormat('en-US', {\n hourCycle: 'h23',\n timeZone: 'America/New_York',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n}).format(new Date('2014-06-25T04:00:00.123Z'));\nconst hourCycleSupported = testDateFormatted === '06/25/2014, 00:00:00' ||\n testDateFormatted === '‎06‎/‎25‎/‎2014‎ ‎00‎:‎00‎:‎00';\nfunction getDateTimeFormat(timeZone) {\n if (!dtfCache[timeZone]) {\n dtfCache[timeZone] = hourCycleSupported\n ? new Intl.DateTimeFormat('en-US', {\n hourCycle: 'h23',\n timeZone: timeZone,\n year: 'numeric',\n month: 'numeric',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n })\n : new Intl.DateTimeFormat('en-US', {\n hour12: false,\n timeZone: timeZone,\n year: 'numeric',\n month: 'numeric',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n });\n }\n return dtfCache[timeZone];\n}\n","import { tzIntlTimeZoneName } from '../../_lib/tzIntlTimeZoneName/index.js';\nimport { tzParseTimezone } from '../../_lib/tzParseTimezone/index.js';\nconst MILLISECONDS_IN_MINUTE = 60 * 1000;\nexport const formatters = {\n // Timezone (ISO-8601. If offset is 0, output is always `'Z'`)\n X: function (date, token, options) {\n const timezoneOffset = getTimeZoneOffset(options.timeZone, date);\n if (timezoneOffset === 0) {\n return 'Z';\n }\n switch (token) {\n // Hours and optional minutes\n case 'X':\n return formatTimezoneWithOptionalMinutes(timezoneOffset);\n // Hours, minutes and optional seconds without `:` delimeter\n // Note: neither ISO-8601 nor JavaScript supports seconds in timezone offsets\n // so this token always has the same output as `XX`\n case 'XXXX':\n case 'XX': // Hours and minutes without `:` delimeter\n return formatTimezone(timezoneOffset);\n // Hours, minutes and optional seconds with `:` delimeter\n // Note: neither ISO-8601 nor JavaScript supports seconds in timezone offsets\n // so this token always has the same output as `XXX`\n case 'XXXXX':\n case 'XXX': // Hours and minutes with `:` delimeter\n default:\n return formatTimezone(timezoneOffset, ':');\n }\n },\n // Timezone (ISO-8601. If offset is 0, output is `'+00:00'` or equivalent)\n x: function (date, token, options) {\n const timezoneOffset = getTimeZoneOffset(options.timeZone, date);\n switch (token) {\n // Hours and optional minutes\n case 'x':\n return formatTimezoneWithOptionalMinutes(timezoneOffset);\n // Hours, minutes and optional seconds without `:` delimeter\n // Note: neither ISO-8601 nor JavaScript supports seconds in timezone offsets\n // so this token always has the same output as `xx`\n case 'xxxx':\n case 'xx': // Hours and minutes without `:` delimeter\n return formatTimezone(timezoneOffset);\n // Hours, minutes and optional seconds with `:` delimeter\n // Note: neither ISO-8601 nor JavaScript supports seconds in timezone offsets\n // so this token always has the same output as `xxx`\n case 'xxxxx':\n case 'xxx': // Hours and minutes with `:` delimeter\n default:\n return formatTimezone(timezoneOffset, ':');\n }\n },\n // Timezone (GMT)\n O: function (date, token, options) {\n const timezoneOffset = getTimeZoneOffset(options.timeZone, date);\n switch (token) {\n // Short\n case 'O':\n case 'OO':\n case 'OOO':\n return 'GMT' + formatTimezoneShort(timezoneOffset, ':');\n // Long\n case 'OOOO':\n default:\n return 'GMT' + formatTimezone(timezoneOffset, ':');\n }\n },\n // Timezone (specific non-location)\n z: function (date, token, options) {\n switch (token) {\n // Short\n case 'z':\n case 'zz':\n case 'zzz':\n return tzIntlTimeZoneName('short', date, options);\n // Long\n case 'zzzz':\n default:\n return tzIntlTimeZoneName('long', date, options);\n }\n },\n};\nfunction getTimeZoneOffset(timeZone, originalDate) {\n const timeZoneOffset = timeZone\n ? tzParseTimezone(timeZone, originalDate, true) / MILLISECONDS_IN_MINUTE\n : originalDate?.getTimezoneOffset() ?? 0;\n if (Number.isNaN(timeZoneOffset)) {\n throw new RangeError('Invalid time zone specified: ' + timeZone);\n }\n return timeZoneOffset;\n}\nfunction addLeadingZeros(number, targetLength) {\n const sign = number < 0 ? '-' : '';\n let output = Math.abs(number).toString();\n while (output.length < targetLength) {\n output = '0' + output;\n }\n return sign + output;\n}\nfunction formatTimezone(offset, delimiter = '') {\n const sign = offset > 0 ? '-' : '+';\n const absOffset = Math.abs(offset);\n const hours = addLeadingZeros(Math.floor(absOffset / 60), 2);\n const minutes = addLeadingZeros(Math.floor(absOffset % 60), 2);\n return sign + hours + delimiter + minutes;\n}\nfunction formatTimezoneWithOptionalMinutes(offset, delimiter) {\n if (offset % 60 === 0) {\n const sign = offset > 0 ? '-' : '+';\n return sign + addLeadingZeros(Math.abs(offset) / 60, 2);\n }\n return formatTimezone(offset, delimiter);\n}\nfunction formatTimezoneShort(offset, delimiter = '') {\n const sign = offset > 0 ? '-' : '+';\n const absOffset = Math.abs(offset);\n const hours = Math.floor(absOffset / 60);\n const minutes = absOffset % 60;\n if (minutes === 0) {\n return sign + String(hours);\n }\n return sign + String(hours) + delimiter + addLeadingZeros(minutes, 2);\n}\n","import type { SupabaseClient } from \"@supabase/supabase-js\";\nimport { isBefore } from \"date-fns\";\nimport { formatInTimeZone, toDate } from \"date-fns-tz\";\nimport type { Location, LocationClosure } from \"../interfaces\";\n\nexport type LocationOrderingKind = \"orders\" | \"catering\";\n\n/** Same opinionated TZ as legacy menu checks. */\nexport const LOCATION_ORDERING_TIMEZONE = \"America/New_York\";\n\nconst DAYS = [\n\t\"sunday\",\n\t\"monday\",\n\t\"tuesday\",\n\t\"wednesday\",\n\t\"thursday\",\n\t\"friday\",\n\t\"saturday\",\n] as const;\n\n/** The open/close instants of one calendar day's window, in LOCATION_ORDERING_TIMEZONE. */\nexport interface LocationWindow {\n\t/** Lowercase weekday the window's hours were read from. */\n\tday: (typeof DAYS)[number];\n\topen: Date;\n\tclose: Date;\n}\n\n/**\n * \"11:00:00+00\" -> \"11:00\". The stored offset is an artifact of the\n * `time with time zone` column; the digits are the location's wall clock.\n */\nexport function wallTime(value: string): string {\n\treturn value.replace(/[+-].*$/, \"\").trim().slice(0, 5);\n}\n\n/**\n * The open/close instants for the day `dayOffset` days from `at`, read in\n * LOCATION_ORDERING_TIMEZONE. Null when that weekday has no hours set.\n *\n * A close at or before the open is read as closing after midnight, so an\n * 11pm–2am window is one continuous night rather than a closed day.\n */\nexport function locationWindowOn(\n\tat: Date,\n\tlocation: Location,\n\tdayOffset = 0,\n): LocationWindow | null {\n\t// Anchored at noon UTC so stepping whole days can never cross a date\n\t// boundary the way adding 24h to a midnight-adjacent instant can.\n\tconst anchor = new Date(\n\t\t`${formatInTimeZone(at, LOCATION_ORDERING_TIMEZONE, \"yyyy-MM-dd\")}T12:00:00Z`,\n\t);\n\tanchor.setUTCDate(anchor.getUTCDate() + dayOffset);\n\tconst dateStr = anchor.toISOString().slice(0, 10);\n\tconst day = DAYS[anchor.getUTCDay()]!;\n\n\tconst open = location[`${day}_open` as keyof Location] as string | undefined;\n\tconst close = location[`${day}_close` as keyof Location] as string | undefined;\n\tif (!open || !close) return null;\n\n\tconst openAt = toDate(`${dateStr}T${wallTime(open)}`, {\n\t\ttimeZone: LOCATION_ORDERING_TIMEZONE,\n\t});\n\tlet closeAt = toDate(`${dateStr}T${wallTime(close)}`, {\n\t\ttimeZone: LOCATION_ORDERING_TIMEZONE,\n\t});\n\tif (closeAt <= openAt) closeAt = new Date(closeAt.getTime() + 24 * 60 * 60 * 1000);\n\n\treturn { day, open: openAt, close: closeAt };\n}\n\n/**\n * True when the location is outside its weekly open window at `at`\n * (includes `force_close`). Yesterday's window is checked too, so the small\n * hours of an overnight window still count as open.\n */\nexport function isLocationClosedByWeeklySchedule(at: Date, location: Location): boolean {\n\tif (location.force_close) return true;\n\n\tfor (const offset of [-1, 0]) {\n\t\tconst window = locationWindowOn(at, location, offset);\n\t\tif (window && !isBefore(at, window.open) && isBefore(at, window.close)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\n/**\n * True when `location_closures` marks the calendar day closed for the given flow.\n */\nexport function isLocationClosedByClosuresTable(\n\tat: Date,\n\tlocationId: number,\n\tclosures: LocationClosure[],\n\tkind: LocationOrderingKind,\n): boolean {\n\tconst day = formatInTimeZone(at, LOCATION_ORDERING_TIMEZONE, \"yyyy-MM-dd\");\n\n\tfor (const row of closures) {\n\t\tif (row.location_id !== locationId) continue;\n\t\tconst rowDay = formatInTimeZone(new Date(row.date), LOCATION_ORDERING_TIMEZONE, \"yyyy-MM-dd\");\n\t\tif (rowDay !== day) continue;\n\t\tif (kind === \"orders\" && Boolean(row.orders_closed)) return true;\n\t\tif (kind === \"catering\" && Boolean(row.catering_closed)) return true;\n\t}\n\treturn false;\n}\n\nexport interface LocationOrderingBlockedOptions {\n\tkind?: LocationOrderingKind;\n\t/** When true (default), times before the current instant are blocked. */\n\tblockIfInPast?: boolean;\n}\n\n/**\n * Single client-side check: blocked if missing location, in the past (optional),\n * `force_close`, outside weekly hours, or `location_closures` for that calendar day.\n */\nexport function isLocationOrderingBlockedAt(\n\tat: Date,\n\tlocation: Location | null,\n\tclosures: LocationClosure[],\n\toptions?: LocationOrderingBlockedOptions,\n): boolean {\n\tif (!location || location.location_id === 0) return true;\n\n\tconst kind = options?.kind ?? \"orders\";\n\tconst blockIfInPast = options?.blockIfInPast !== false;\n\tif (blockIfInPast && at.getTime() < Date.now()) {\n\t\treturn true;\n\t}\n\n\tif (isLocationClosedByWeeklySchedule(at, location)) return true;\n\tif (isLocationClosedByClosuresTable(at, location.location_id, closures, kind)) return true;\n\n\treturn false;\n}\n\n/**\n * Loads `location_closures` rows for the given location ids (Supabase anon / user client).\n */\nexport async function fetchLocationClosuresForLocations(\n\tclient: SupabaseClient,\n\tlocationIds: number[],\n): Promise<LocationClosure[]> {\n\tif (locationIds.length === 0) return [];\n\n\tconst { data, error } = await client\n\t\t.from(\"location_closures\")\n\t\t.select(\"id, franchise_id, location_id, orders_closed, catering_closed, date\")\n\t\t.in(\"location_id\", locationIds);\n\n\tif (error) {\n\t\tconsole.error(\"[fetchLocationClosuresForLocations]\", error.message);\n\t\treturn [];\n\t}\n\n\treturn (data ?? []) as LocationClosure[];\n}\n","import type { Menu } from \"@zorgo/universal/utils/menuUtils\";\nimport type { ItemVariant, Location, MenuItem } from \"@zorgo/universal/interfaces\";\nimport { wallTime } from \"@zorgo/universal/utils/locationOrderingAvailability\";\n\n/** Franchise-level facts for the Restaurant node. Everything but `name` is\n * optional so the schema degrades gracefully when a field is missing. */\nexport interface RestaurantInfo {\n\tname: string;\n\turl?: string;\n\timage?: string;\n\tdescription?: string;\n\tservesCuisine?: string[];\n\t/** Public locations from `lib/generated/locations.ts`. One location's facts\n\t * land on the Restaurant node itself; several become linked branches. */\n\tlocations?: Location[];\n}\n\nconst DAYS = [\n\t\"Monday\",\n\t\"Tuesday\",\n\t\"Wednesday\",\n\t\"Thursday\",\n\t\"Friday\",\n\t\"Saturday\",\n\t\"Sunday\",\n] as const;\n\n/** Stable node ids so the parent and its branches can reference each other. */\nfunction restaurantId(info: RestaurantInfo) {\n\treturn `${info.url ?? \"\"}#restaurant`;\n}\nfunction branchId(info: RestaurantInfo, location: Location) {\n\treturn `${info.url ?? \"\"}#location-${location.location_id}`;\n}\n\n/**\n * One OpeningHoursSpecification per distinct weekly window, with the days that\n * share it grouped into `dayOfWeek`. Days with no hours set are simply absent,\n * which is how schema.org expresses \"closed\".\n */\nexport function openingHoursSpecification(location: Location) {\n\tconst byWindow = new Map<string, string[]>();\n\tfor (const day of DAYS) {\n\t\tconst key = day.toLowerCase();\n\t\tconst open = location[`${key}_open` as keyof Location] as string | undefined;\n\t\tconst close = location[`${key}_close` as keyof Location] as string | undefined;\n\t\tif (!open || !close) continue;\n\t\tconst window = `${wallTime(open)}|${wallTime(close)}`;\n\t\tbyWindow.set(window, [...(byWindow.get(window) ?? []), `https://schema.org/${day}`]);\n\t}\n\n\treturn [...byWindow].map(([window, dayOfWeek]) => {\n\t\tconst [opens, closes] = window.split(\"|\");\n\t\treturn { \"@type\": \"OpeningHoursSpecification\", dayOfWeek, opens, closes };\n\t});\n}\n\n/**\n * `123 Main St, Springfield, IL 62704` — street, city, two-letter state, ZIP,\n * with an optional +4 and an optional trailing country. Anchored at both ends\n * and deliberately strict: everything before the last two commas is the street,\n * so a unit (\"… Ste 4, Springfield, IL 62704\") still parses.\n */\nconst US_ADDRESS = /^(.+),\\s*([^,]+),\\s*([A-Z]{2})\\s+(\\d{5}(?:-\\d{4})?)(?:,\\s*USA?)?$/;\n\n/**\n * schema.org accepts `address` as either Text or a PostalAddress node, but\n * Google's LocalBusiness rich results only read the structured form — a bare\n * string means the branch nodes never qualify.\n *\n * `address` is still one staff-typed column, so this parses rather than reads,\n * and it only parses what it can prove: a string that does not match the US\n * shape exactly falls back to the Text form it has always shipped. A wrong\n * `addressRegion` in structured data is worse than no `addressRegion` — it is a\n * fact we are asserting to Google — so the regex declines rather than guesses.\n *\n * ponytail: US-only, because every franchise is. Drop the fallback and read real\n * columns the day `locations` stores the parts separately.\n */\nfunction postalAddress(address: string): Record<string, unknown> | string {\n\tconst match = US_ADDRESS.exec(address.trim());\n\tif (!match) return address;\n\tconst [, streetAddress, addressLocality, addressRegion, postalCode] = match;\n\t// Unreachable — all four groups are required by the pattern — but under\n\t// noUncheckedIndexedAccess a capture is `string | undefined`, and declining\n\t// is already what this function does when it cannot prove the shape.\n\tif (!streetAddress || !addressLocality || !addressRegion || !postalCode) return address;\n\treturn {\n\t\t\"@type\": \"PostalAddress\",\n\t\tstreetAddress: streetAddress.trim(),\n\t\taddressLocality: addressLocality.trim(),\n\t\taddressRegion,\n\t\tpostalCode,\n\t\taddressCountry: \"US\",\n\t};\n}\n\n/** address / geo / telephone / hours — the facts shared by the single-location\n * Restaurant node and every branch node. */\nfunction locationFacts(location: Location) {\n\tconst facts: Record<string, unknown> = {};\n\tif (location.address) facts.address = postalAddress(location.address);\n\tif (location.phone) facts.telephone = location.phone;\n\tif (location.lat != null && location.lng != null) {\n\t\tfacts.geo = {\n\t\t\t\"@type\": \"GeoCoordinates\",\n\t\t\tlatitude: location.lat,\n\t\t\tlongitude: location.lng,\n\t\t};\n\t}\n\tconst hours = openingHoursSpecification(location);\n\tif (hours.length) facts.openingHoursSpecification = hours;\n\treturn facts;\n}\n\n/**\n * The branch node for one location, for that location's page. Carries the same\n * address/geo/hours facts as the parent and links back to it via\n * `parentOrganization`. Pure — safe to run at build time.\n */\nexport function buildLocationJsonLd(location: Location, info: RestaurantInfo) {\n\tconst node: Record<string, unknown> = {\n\t\t\"@context\": \"https://schema.org\",\n\t\t\"@type\": \"Restaurant\",\n\t\t\"@id\": branchId(info, location),\n\t\tname: `${info.name} — ${location.name}`,\n\t\tparentOrganization: { \"@id\": restaurantId(info) },\n\t\t...locationFacts(location),\n\t};\n\tif (info.image) node.image = info.image;\n\treturn node;\n}\n\n// cents -> \"5.49\" (schema.org Offer.price is a string in major currency units)\nfunction priceString(cents: number): string {\n\treturn (cents / 100).toFixed(2);\n}\n\nfunction variantOffer(variant: ItemVariant) {\n\treturn {\n\t\t\"@type\": \"Offer\",\n\t\tprice: priceString(variant.display_price ?? variant.price),\n\t\tpriceCurrency: \"USD\",\n\t};\n}\n\nfunction menuItemNode(item: MenuItem) {\n\tconst node: Record<string, unknown> = {\n\t\t\"@type\": \"MenuItem\",\n\t\tname: item.name,\n\t};\n\tif (item.description) node.description = item.description;\n\t// one Offer for a single price, an array when the item has size/variant prices\n\tconst offers = item.variants.map(variantOffer);\n\tif (offers.length === 1) node.offers = offers[0];\n\telse if (offers.length > 1) node.offers = offers;\n\treturn node;\n}\n\n/**\n * Builds a standalone schema.org JSON-LD object for a single menu item, for\n * embedding in that item's `/menu/[slug]` page `<head>`. Reuses `menuItemNode`\n * (the same node shape used inside the Restaurant menu) and adds the\n * `@context` a top-level node needs. Pure — safe to run at build time.\n */\nexport function buildMenuItemJsonLd(item: MenuItem) {\n\treturn { \"@context\": \"https://schema.org\", ...menuItemNode(item) };\n}\n\n/**\n * Builds a schema.org Restaurant JSON-LD object from a prebuilt Menu and\n * franchise info. Categories become MenuSections; the items in each become\n * MenuItems. Pure — same inputs, same output — so it can run at prebuild time\n * and be embedded straight into the site's <head>.\n */\nexport function buildRestaurantJsonLd(menu: Menu, info: RestaurantInfo) {\n\tconst hasMenuSection = menu.categories.map((category) => {\n\t\tconst section: Record<string, unknown> = {\n\t\t\t\"@type\": \"MenuSection\",\n\t\t\tname: category.name,\n\t\t};\n\t\tconst items = menu.items\n\t\t\t.filter((item) => item.category_id === category.category_id)\n\t\t\t.map(menuItemNode);\n\t\tif (items.length) section.hasMenuItem = items;\n\t\treturn section;\n\t});\n\n\tconst restaurant: Record<string, unknown> = {\n\t\t\"@context\": \"https://schema.org\",\n\t\t\"@type\": \"Restaurant\",\n\t\tname: info.name,\n\t};\n\tif (info.url) restaurant.url = info.url;\n\tif (info.image) restaurant.image = info.image;\n\tif (info.description) restaurant.description = info.description;\n\tif (info.servesCuisine?.length) restaurant.servesCuisine = info.servesCuisine;\n\n\t// A single-location franchise *is* its storefront, so its facts belong on\n\t// the Restaurant node. Several locations get referenced as branches instead\n\t// — the full branch nodes live on the location pages, not duplicated here.\n\tconst locations = info.locations ?? [];\n\tif (locations.length === 1) {\n\t\tObject.assign(restaurant, locationFacts(locations[0]!));\n\t} else if (locations.length > 1) {\n\t\trestaurant[\"@id\"] = restaurantId(info);\n\t\trestaurant.subOrganization = locations.map((location) => ({\n\t\t\t\"@type\": \"Restaurant\",\n\t\t\t\"@id\": branchId(info, location),\n\t\t\tname: location.name,\n\t\t}));\n\t}\n\n\trestaurant.hasMenu = {\n\t\t\"@type\": \"Menu\",\n\t\tname: \"Menu\",\n\t\thasMenuSection,\n\t};\n\n\treturn restaurant;\n}\n","// Prebuild-only image encoding. Two producers, one encoder:\n//\n// stampItemImages — downloads each menu item image from the public bucket,\n// writes its ladder to public/images/items/, and stamps\n// `local_key` onto the record so the generated menu\n// module points at the local variants.\n// optimizeSiteImages — walks the site's own originals in assets/images/ (kept\n// out of public/ so the full-res files never reach the\n// deploy bundle) and writes their ladders to\n// public/images/.\n//\n// Both emit `<base>-<width>.avif`, which is what the loader in ../images.ts\n// resolves an extension-less `/images/...` src to.\n//\n// Runs in the site's cwd during scripts/prebuild.ts — never in the browser.\nimport { existsSync, mkdirSync, readdirSync, statSync } from \"fs\";\nimport { readFile } from \"fs/promises\";\nimport { createRequire } from \"module\";\nimport path from \"path\";\nimport type { BaseItemImage } from \"@zorgo/universal/interfaces\";\nimport type { Menu } from \"@zorgo/universal/utils/menuUtils\";\nimport { menuImageUrl } from \"@zorgo/universal/utils/menuUtils\";\nimport { ITEM_IMAGE_PREFIX, IMAGE_WIDTHS } from \"../images\";\n\n/**\n * Encoder and loader share one width list — see images.ts. Changing it requires\n * regenerating every site's variants, or the loader requests a rung that was\n * never written.\n */\nexport { IMAGE_WIDTHS };\n\n/**\n * AVIF only: a custom next/image loader returns a single URL and so cannot do\n * Accept-header format negotiation. AVIF is the smaller of the two and is\n * universally supported in current browsers; anything older falls back to the\n * bucket URL only when the whole download failed, not per-format.\n */\nconst AVIF_QUALITY = 60;\n\n/**\n * Where a site keeps the full-res originals prebuild is allowed to re-encode.\n * Deliberately outside public/ — an original is build input, not an asset, and\n * a 9 MB JPEG sitting in public/ uploads to the Worker on every deploy for\n * nothing.\n */\nexport const SITE_IMAGE_SOURCE_DIR = \"assets/images\";\n\n/** What sharp will read out of that directory. Anything else is left alone. */\nconst SOURCE_EXTENSIONS = /\\.(jpe?g|png|webp|avif|tiff?|gif)$/i;\n\ntype Sharp = typeof import(\"sharp\").default;\n\n/**\n * sharp is pinned in the site's package.json, not this package's, so a bare\n * `import(\"sharp\")` resolves from wherever this bundle happens to live and fails\n * under pnpm's isolated linker. Resolve it from the site root instead — the same\n * thing Next does for its own optimizer.\n */\nfunction loadSharp(root: string): Sharp {\n\t// require, not import(): a computed dynamic import is a module graph edge\n\t// bundlers try to follow, and the site's api/zorgo-token route imports this\n\t// bundle for fetchSiteToken — Turbopack then fails the route on a `sharp` it\n\t// has no reason to resolve. A createRequire call is opaque to them, and this\n\t// line only ever runs in Node during prebuild.\n\tfor (const from of [root, process.cwd()]) {\n\t\ttry {\n\t\t\treturn createRequire(path.join(from, \"package.json\"))(\"sharp\");\n\t\t} catch (err) {\n\t\t\tif ((err as NodeJS.ErrnoException).code !== \"MODULE_NOT_FOUND\") throw err;\n\t\t}\n\t}\n\tthrow new Error(\n\t\t`sharp is not installed under ${root}. It is pinned in every scaffolded site's package.json — run an install there.`,\n\t);\n}\n\n/** Absolute path of one variant file, for any `<base>-<width>.avif` ladder. */\nfunction variantPath(outBase: string, width: number): string {\n\treturn `${outBase}-${width}.avif`;\n}\n\n/**\n * Writes the full AVIF ladder for one source buffer at `<outBase>-<width>.avif`.\n * Throws on an unreadable or undecodable source — every caller catches, because\n * one bad image must not fail a build.\n */\nasync function encodeLadder(source: Buffer, outBase: string, sharp: Sharp): Promise<void> {\n\tmkdirSync(path.dirname(outBase), { recursive: true });\n\tfor (const width of IMAGE_WIDTHS) {\n\t\t// withoutEnlargement: a source narrower than a rung is written at its own\n\t\t// width, so that rung's srcset descriptor overstates it slightly. The\n\t\t// browser then picks a wider candidate than it needs — wasteful in bytes\n\t\t// only when someone uploads a tiny image.\n\t\tawait sharp(source)\n\t\t\t.resize({ width, withoutEnlargement: true })\n\t\t\t.avif({ quality: AVIF_QUALITY })\n\t\t\t.toFile(variantPath(outBase, width));\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Menu item images (downloaded from the bucket)\n// ---------------------------------------------------------------------------\n\n/**\n * Storage path -> a filesystem-safe key that changes whenever the underlying\n * object does. Upload keys are timestamped, so a re-upload yields a new key and\n * can never collide with the previous image's cached variants.\n */\nexport function itemImageLocalKey(image: BaseItemImage): string | null {\n\tconst storagePath = image.processed_path ?? image.raw_path;\n\tif (!storagePath) return null;\n\treturn storagePath\n\t\t.replace(/\\.[^./]+$/, \"\")\n\t\t.replace(/[^a-zA-Z0-9_]+/g, \"-\")\n\t\t.replace(/^-|-$/g, \"\");\n}\n\n/** Absolute path of one item variant file. */\nexport function itemImageVariantPath(root: string, key: string, width: number): string {\n\treturn variantPath(path.join(root, \"public\", ITEM_IMAGE_PREFIX, key), width);\n}\n\n/**\n * Downloads one image and writes its ladder. Returns the key on success, or\n * null on any failure — the caller leaves `local_key` unset and the render\n * falls back to the bucket URL, so a dead image never fails the build.\n *\n * Variants already on disk are kept: keys are content-addressed by upload\n * timestamp, so an existing file is always the right file.\n */\nexport async function materializeItemImage(\n\timage: BaseItemImage,\n\troot: string,\n): Promise<string | null> {\n\tconst key = itemImageLocalKey(image);\n\tif (!key) return null;\n\n\tconst outBase = path.join(root, \"public\", ITEM_IMAGE_PREFIX, key);\n\tif (IMAGE_WIDTHS.every((width) => existsSync(variantPath(outBase, width)))) return key;\n\n\tconst url = menuImageUrl(image, true);\n\tif (!url) return null;\n\n\ttry {\n\t\tconst res = await fetch(url);\n\t\tif (!res.ok) throw new Error(`${res.status} ${res.statusText}`);\n\t\tawait encodeLadder(Buffer.from(await res.arrayBuffer()), outBase, loadSharp(root));\n\t\treturn key;\n\t} catch (err) {\n\t\tconsole.error(`prebuild: image ${image.image_id} (${url}) failed:`, err);\n\t\treturn null;\n\t}\n}\n\n/**\n * Stamps `local_key` onto every image in the menu, in place, so writeMenuModule\n * serializes it. Mutates rather than copies because the menu is a passthrough\n * of unknown API columns — rebuilding it would mean re-describing that shape.\n */\nexport async function stampItemImages(menu: Menu, root: string): Promise<void> {\n\tconst images = menu.items.flatMap((item) => item.images ?? []);\n\tfor (const image of images) {\n\t\timage.local_key = await materializeItemImage(image, root);\n\t}\n\tconst stamped = images.filter((image) => image.local_key).length;\n\tif (images.length) {\n\t\tconsole.log(`prebuild: ${stamped}/${images.length} item images localized`);\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Site-authored images (read off disk)\n// ---------------------------------------------------------------------------\n\n/** Every encodable file under `dir`, as paths relative to it. */\nfunction collectSources(dir: string, prefix = \"\"): string[] {\n\treturn readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {\n\t\tconst rel = prefix ? `${prefix}/${entry.name}` : entry.name;\n\t\tif (entry.isDirectory()) return collectSources(path.join(dir, entry.name), rel);\n\t\treturn SOURCE_EXTENSIONS.test(entry.name) ? [rel] : [];\n\t});\n}\n\n/**\n * `Lemonades_Web_4x6 web.png` -> `Lemonades_Web_4x6-web`, the extension-less\n * path the site references as `/images/<key>`. Directory structure survives;\n * anything that would need escaping in a URL — or a second dot, which would make\n * the loader read the src as an already-extensioned asset and pass it through —\n * does not.\n */\nexport function siteImageKey(relativePath: string): string {\n\treturn relativePath\n\t\t.replace(/\\.[^./]+$/, \"\")\n\t\t.split(\"/\")\n\t\t.map((segment) => segment.replace(/[^a-zA-Z0-9_-]+/g, \"-\").replace(/^-|-$/g, \"\"))\n\t\t.join(\"/\");\n}\n\n/** True when every rung exists and is at least as new as the source. */\nfunction laddersAreFresh(sourcePath: string, outBase: string): boolean {\n\tconst sourceMtime = statSync(sourcePath).mtimeMs;\n\treturn IMAGE_WIDTHS.every((width) => {\n\t\tconst file = variantPath(outBase, width);\n\t\treturn existsSync(file) && statSync(file).mtimeMs >= sourceMtime;\n\t});\n}\n\n/**\n * Encodes every original in `<root>/assets/images/` into public/images/, so the\n * site can reference `/images/<key>` and get the ladder. No-op for a site that\n * has no such directory.\n *\n * Re-encodes only what changed: a variant older than its source is stale, and\n * mtime is the right signal here because these files are edited in place (unlike\n * item images, whose keys are content-addressed).\n *\n * ponytail: never deletes. Renaming a source leaves its old variants in\n * public/images/ to deploy as dead weight — clear them by hand, or add a sweep\n * here if it starts mattering.\n */\nexport async function optimizeSiteImages(root: string): Promise<void> {\n\tconst sourceDir = path.join(root, SITE_IMAGE_SOURCE_DIR);\n\tif (!existsSync(sourceDir)) return;\n\n\tconst sources = collectSources(sourceDir);\n\tif (sources.length === 0) return;\n\n\tconst sharp = loadSharp(root);\n\tconst claimed = new Map<string, string>();\n\tlet encoded = 0;\n\tlet failed = 0;\n\n\tfor (const relativePath of sources) {\n\t\tconst key = siteImageKey(relativePath);\n\n\t\t// Two sources can sanitize to one key (food.jpg + food.png). Skipping the\n\t\t// second is arbitrary, but silently overwriting the first is worse.\n\t\tconst clash = claimed.get(key);\n\t\tif (clash) {\n\t\t\tconsole.warn(\n\t\t\t\t`prebuild: ${relativePath} and ${clash} both map to /images/${key} — skipping ${relativePath}`,\n\t\t\t);\n\t\t\tcontinue;\n\t\t}\n\t\tclaimed.set(key, relativePath);\n\n\t\tconst sourcePath = path.join(sourceDir, relativePath);\n\t\tconst outBase = path.join(root, \"public\", \"images\", key);\n\t\tif (laddersAreFresh(sourcePath, outBase)) continue;\n\n\t\ttry {\n\t\t\tawait encodeLadder(await readFile(sourcePath), outBase, sharp);\n\t\t\tencoded++;\n\t\t} catch (err) {\n\t\t\tfailed++;\n\t\t\tconsole.error(`prebuild: site image ${relativePath} failed:`, err);\n\t\t}\n\t}\n\n\tconsole.log(\n\t\t`prebuild: ${sources.length} site images (${encoded} encoded, ` +\n\t\t\t`${sources.length - encoded - failed} cached, ${failed} failed)`,\n\t);\n}\n","// Env first, hardcoded production second — the same shape POS and kiosk already\n// use in their own config/environment.ts. Both bundlers inline these by literal\n// name (Next NEXT_PUBLIC_*, Expo EXPO_PUBLIC_*), so the lookups have to stay\n// written out; a destructured or computed read is not substituted. Without this\n// the two vars zorgo_site/.env already declares were dead, and pointing any app\n// at a Supabase preview branch meant editing this file.\nexport const SUPABASE_URL =\n\tprocess.env.NEXT_PUBLIC_SUPABASE_URL ||\n\tprocess.env.EXPO_PUBLIC_SUPABASE_URL ||\n\t\"https://dhnpoxorllfdobwftkyb.supabase.co\";\n\nexport const SUPABASE_ANON_KEY =\n\tprocess.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ||\n\tprocess.env.EXPO_PUBLIC_SUPABASE_ANON_KEY ||\n\t\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRobnBveG9ybGxmZG9id2Z0a3liIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzkwNTQxMDgsImV4cCI6MjA1NDYzMDEwOH0.zI8PwbgKwijHZ1QmxJqmaPrFvg02w1vOY2Qcs7SddzI\";\nexport const ZORGO_PURPLE = \"rgb(153,67,233)\";\n\nexport const expoGoGoogleClientId =\n\t\"959654249401-dm6nbi27rj605ul1nkt8o2imb8f7daff.apps.googleusercontent.com\";\n\n// replace instances of API_BASE_URL with env enum\nexport enum Environments {\n\tProduction = \"https://api.zorgotech.com\",\n\tStaging = \"https://staging.zorgo.ai\",\n\tDevelopment = \"http://localhost:3000\",\n}\n\nexport const API_BASE_URL =\n\tprocess.env.NODE_ENV === \"production\"\n\t\t? Environments.Production\n\t\t: Environments.Development;\n\n/** Emails allowed to access developer-only metrics (e.g. /performance/developer). */\nexport const DEVELOPER_EMAILS = [\"clawsorgo@gmail.com\", \"gmandwee@gmail.com\"] as const;\n\nexport function isDeveloperEmail(email?: string | null): boolean {\n\treturn !!email && (DEVELOPER_EMAILS as readonly string[]).includes(email);\n}\n\nexport const ORDER_STORE_CONSTANTS = {\n\tDEFAULTS: {\n\t\tPREP_MINUTES: 20, // default amount of time for prep\n\t\tPAST_HOURS: 24, // hours into the past orders are fetched for\n\t\tFUTURE_HOURS: 72 // hours into the future orders are fetched for \n\t},\n\tORDER_STATUS: {\n\t\tSTAGING: 10,\n\t\tPENDING: 1,\n\t\tCOMPLETED: 2,\n\t\tCANCELLED: 4,\n\t\tPAID: 6,\n\t\tPARTIALLY_PAID: 7,\n\t\tKITCHEN_DONE: 9,\n\t\tCONFIRMED: 11,\n\t\tPREPARING: 12,\n\t\tREQUESTED: 13,\n\t},\n\tPOS_METHOD_ID: 3,\n\t/**\n\t * Internal QA accounts — their orders are excluded from dashboard order queries.\n\t * Matched on dev/QA emails (kcjayd@, clawsorgo@, gmandwee@, aaiden798@, *@zorgo.*),\n\t * the \"c d\" throwaway persona, and known test names (casey donegan, aiden alazo, …).\n\t * ponytail: hardcoded snapshot — new test accounts need re-running the query.\n\t * Promote to an `is_test` column on `customers` if this list keeps growing.\n\t */\n\tTEST_CUSTOMER_IDS: [\n\t\t85, 90, 91, 93, 95, 96, 97, 203, 238, 240, 241, 244, 245, 246, 317, 318, 319, 322,\n\t\t579, 580, 658, 1526, 2567, 2573, 2703, 2725, 2737, 2739, 2740, 2741, 2742, 2744,\n\t\t2746, 2747, 2749, 2751, 2752, 2753, 2754, 2755, 2767, 2768, 2778, 2779, 2780, 2781,\n\t\t2783, 2814, 2816, 2818, 2819, 2820, 2821, 2822, 2823, 2824, 2835, 2836, 2851, 2853,\n\t\t2859, 2871, 2882, 2883, 2887, 2888, 2889, 2891, 2892, 2893, 2894, 2895, 2896, 2897,\n\t\t2898, 2899, 2900, 2901, 2902, 2903, 2904, 2905, 2907, 2908, 2919, 2921, 2922, 2923,\n\t\t2924, 2925, 2926, 2927, 2946, 3017, 3018, 3022, 3024, 3025, 3026, 3027, 3028, 3029,\n\t\t3030, 3031, 3082, 3091, 3092, 3093,\n\t],\n\tORDER_TYPE: {\n\t\tDINE_IN: 1,\n\t\tTAKEOUT: 2,\n\t\tDELIVERY: 3\n\t},\n\t/** KDS edit highlight duration after a POS modification. */\n\tMODIFICATION_HIGHLIGHT_MS: 8_000,\n\t/** POS order realtime: refetch + recycle postgres_changes channel on this cadence. */\n\tLISTENER_SYNC_INTERVAL_MS: 60_000,\n\tORDER_SELECT: `\n\t*,\n\n\tcharges (\n\t\t*\n\t),\n\n\tlocations (name),\n\n\tcustomers\n\t (customer_id, first_name, last_name, phone_number, email),\n\n\torder_items\n\t (\n\t\torder_item_id, item_id, quantity, price_at_order, order_id, comments, status, applied_promo_id, discount_amount,\n\t\titems (\n\t\t\titem_id, name, price,\n\t\t\tportion_schemes (id, franchise_id, geometry, name, min_divisions, max_divisions),\n\t\t\tbase_items (\n\t\t\t\tbase_item_id, category_id, name,\n\t\t\t\tcategories (category_id, name, is_catering)\n\t\t\t)\n\t\t),\n\t\torder_item_selected_items (\n\t\t\torder_item_id, rule_id, item_id, quantity, upcharge_price, portion,\n\t\t\titems (\n\t\t\t\titem_id, name, price,\n\t\t\t\tbase_items (\n\t\t\t\t\tbase_item_id, category_id, name,\n\t\t\t\t\tcategories (category_id, name, is_catering)\n\t\t\t\t),\n\t\t\t\titem_components (\n\t\t\t\t\tservings_per_item,\n\t\t\t\t\tcomponent_quantity_behaviors (headcount_threshold, starting_amount, step_size, step_interval, maximum),\n\t\t\t\t\tcomponents (id, name, component_type_id, serving_weight, measurement_type, display_unit)\n\t\t\t\t)\n\t\t\t),\n\t\t\titem_rules (rule_id, group_name)\n\t\t),\n\t\torder_item_selected_components (\n\t\t\torder_item_id, rule_id, component_id, quantity, upcharge_price, portion,\n\t\t\tcomponents (id, name, component_type_id, serving_weight, measurement_type, display_unit), item_rules (rule_id, group_name)\n\t\t),\n\t\torder_item_components (\n\t\t\torder_item_id, component_id, quantity_servings, is_base, portion,\n\t\t\tcomponents (id, name, component_type_id, serving_weight, measurement_type, display_unit)\n\t\t)\n\t)\n`,\n};\n\nexport const CHARGE_STATUSES = {\n\tSUCCESS: 1,\n\tPENDING: 2,\n\tREFUNDED: 3,\n\tVOIDED: 4,\n\tPAYMENT_QUEUED: 5,\n\tFAILED: 6\n}\n","import { SupabaseClient } from \"@supabase/supabase-js\";\nimport { getDay, isAfter, isBefore, parse, startOfDay } from \"date-fns\";\nimport { SUPABASE_URL } from \"../constants\";\nimport {\n\tBaseItem,\n\tBaseItemImage,\n\tCategory,\n\tComponent,\n\tItemVariant,\n\tMenuItem,\n\tPlatforms,\n\tPromotion,\n\tRule,\n\tVisibilityConditionGroupAssignment,\n\tVisibilityEffect,\n} from \"../interfaces\";\nimport { csvFormat } from \"./misc\";\nimport {\n\tbuildVisibilityEvaluationContext,\n\tevaluateVisibilityCondition,\n} from \"./visibilityConditions\";\n\nexport interface Menu {\n\titems: MenuItem[];\n\tcategories: Category[];\n\tcomponents: Component[];\n\trules: Rule[];\n}\n\nexport interface MenuIds {\n\tbaseItemIds: number[];\n\tcategoryIds: number[];\n\titemIds: number[];\n}\n\ntype MenuIdRows = {\n\tcategories: Pick<Category, \"category_id\">[];\n\tbaseItems: Pick<BaseItem, \"base_item_id\" | \"category_id\">[];\n\titems: Pick<ItemVariant, \"item_id\" | \"base_item_id\">[];\n};\n\n/**\n * Menu display order: curated `sort_index` first, unset rows after it.\n *\n * `nullsFirst: false` is the load-bearing half. A franchise pins only the few\n * things it cares about and leaves everything else NULL, so nulls have to fall\n * to the bottom where the tiebreak column (name, or price for variants) sorts\n * them — otherwise the unpinned majority would sit above the curated rows.\n */\nconst ORDER_CURATED = { ascending: true, nullsFirst: false } as const;\n\n/** Sorts last, so an unset sort_index yields to whatever the caller tiebreaks on. */\nconst UNSORTED = Number.MAX_SAFE_INTEGER;\n\n/**\n * The in-memory half of ORDER_CURATED, for rows Postgres can't order for us: a\n * rule's options are one list to the customer but live in two tables\n * (rule_items, rule_components), and their display names sit a join further out\n * on items/components, which PostgREST can't order an embed by.\n */\nfunction byCuratedThenName(\n\ta: { sort_index?: number | null; name: string },\n\tb: { sort_index?: number | null; name: string },\n): number {\n\tconst delta = (a.sort_index ?? UNSORTED) - (b.sort_index ?? UNSORTED);\n\treturn delta !== 0 ? delta : a.name.localeCompare(b.name);\n}\n\nexport async function getMenu(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n\texclusions: MenuIds = emptyMenuExclusions(),\n\tuseBehaviors = false,\n): Promise<Menu> {\n\tconst categories = await getCategories(\n\t\tsupabase,\n\t\tfranchiseId,\n\t\texclusions.categoryIds,\n\t);\n\n\tif (categories.length === 0) {\n\t\treturn buildMenu([], [], [], [], []);\n\t}\n\n\tconst baseItems = await getBaseItems(\n\t\tsupabase,\n\t\tfranchiseId,\n\t\texclusions.baseItemIds,\n\t\tcategories,\n\t);\n\n\tif (baseItems.length === 0) {\n\t\treturn buildMenu([], [], categories, [], []);\n\t}\n\n\tconst items = await getItems(supabase, baseItems, exclusions.itemIds);\n\tconst components = await getComponents(supabase, items, useBehaviors);\n\tconst rules = await getRules(supabase, baseItems, categories, items, useBehaviors);\n\n\treturn buildMenu(items, baseItems, categories, components, rules);\n}\n\nexport async function getCateringBaseItemIds(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n): Promise<number[]> {\n\tconst { data: categories } = await supabase\n\t\t.from(\"categories\")\n\t\t.select(\"category_id\")\n\t\t.eq(\"franchise_id\", franchiseId)\n\t\t.eq(\"is_catering\", true)\n\t\t.throwOnError();\n\n\tif (!categories || categories.length === 0) {\n\t\treturn [];\n\t}\n\n\tconst { data } = await supabase\n\t\t.from(\"base_items\")\n\t\t.select(\"base_item_id\")\n\t\t.eq(\"franchise_id\", franchiseId)\n\t\t.in(\"category_id\", categories.map((category) => category.category_id))\n\t\t.throwOnError();\n\treturn data.map((row) => row.base_item_id);\n}\n\n/**\n * Attaches targeted promotions to menu cards without mutating the menu.\n * Order-wide promos intentionally stay out of menu display pricing.\n */\nexport function fitPromosToMenu(\n\tmenu: Menu,\n\tactivePromotions: Promotion[],\n): Menu {\n\tconst categoryPromoIds = new Map<number, number>();\n\tconst getDisplayPrice = (price: number, promo: Promotion) => {\n\t\tif (promo.promotion_type_id === 1) {\n\t\t\treturn Math.max(0, Math.floor(price * (1 - promo.discount_value / 100)));\n\t\t}\n\t\tif (promo.promotion_type_id === 2) {\n\t\t\treturn Math.max(0, price - promo.discount_value);\n\t\t}\n\t\treturn price;\n\t};\n\n\tconst items = menu.items.map((item) => {\n\t\t// Bulk discount promos (quantity-dependent sigmoid) attach to the item but never set a fixed display_price\n\t\tconst bulkPromo = activePromotions.find((promo) => {\n\t\t\tif (!promo.is_active || promo.id == null || !promo.discount_target_ids?.length || !promo.min_purchase_quantity) return false;\n\t\t\tconst targets = promo.discount_target_ids;\n\t\t\treturn (\n\t\t\t\t(promo.discount_target_type === \"category\" && targets.includes(item.category_id)) ||\n\t\t\t\t(promo.discount_target_type === \"item\" && targets.includes(item.base_item_id))\n\t\t\t);\n\t\t});\n\n\t\tlet itemPromoId: number | undefined = bulkPromo?.id;\n\n\t\tconst variants = item.variants.map((variant) => {\n\t\t\tlet bestPromo: Promotion | undefined;\n\t\t\tlet bestDisplayPrice = variant.price;\n\n\t\t\tfor (const promo of activePromotions) {\n\t\t\t\tif (\n\t\t\t\t\t!promo.is_active ||\n\t\t\t\t\tpromo.id == null ||\n\t\t\t\t\t!promo.discount_target_ids?.length ||\n\t\t\t\t\tpromo.min_purchase_amount || // order-level threshold — shown via modal, not baked into menu price\n\t\t\t\t\tpromo.min_purchase_quantity // quantity-dependent — handled above, no fixed display_price\n\t\t\t\t) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst targets = promo.discount_target_ids;\n\t\t\t\tconst applies =\n\t\t\t\t\t(promo.discount_target_type === \"category\" &&\n\t\t\t\t\t\ttargets.includes(item.category_id)) ||\n\t\t\t\t\t(promo.discount_target_type === \"item\" &&\n\t\t\t\t\t\ttargets.includes(item.base_item_id)) ||\n\t\t\t\t\t(promo.discount_target_type === \"item_variant\" &&\n\t\t\t\t\t\ttargets.includes(variant.item_id));\n\n\t\t\t\tif (!applies) continue;\n\n\t\t\t\tconst displayPrice = getDisplayPrice(variant.price, promo);\n\t\t\t\tif (displayPrice < bestDisplayPrice) {\n\t\t\t\t\tbestPromo = promo;\n\t\t\t\t\tbestDisplayPrice = displayPrice;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!bestPromo || bestDisplayPrice >= variant.price) {\n\t\t\t\treturn variant;\n\t\t\t}\n\n\t\t\titemPromoId ??= bestPromo.id!;\n\t\t\tif (bestPromo.discount_target_type === \"category\") {\n\t\t\t\tcategoryPromoIds.set(item.category_id, bestPromo.id!);\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\t...variant,\n\t\t\t\tactive_promo_id: bestPromo.id!,\n\t\t\t\tdisplay_price: bestDisplayPrice,\n\t\t\t};\n\t\t});\n\n\t\treturn { ...item, variants, active_promo_id: itemPromoId };\n\t});\n\n\tconst categories = menu.categories.map((category) => {\n\t\tconst { active_promo_id, ...cleanCategory } = category;\n\t\tvoid active_promo_id;\n\n\t\tconst promoId = categoryPromoIds.get(cleanCategory.category_id);\n\n\t\treturn promoId == null\n\t\t\t? cleanCategory\n\t\t\t: { ...cleanCategory, active_promo_id: promoId };\n\t});\n\n\treturn {\n\t\t...menu,\n\t\titems,\n\t\tcategories,\n\t};\n}\n\nexport async function getMenuExclusions(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n\tlocationId: number,\n\tplatform: Platforms,\n): Promise<MenuIds> {\n\tvoid locationId;\n\n\tconst menuIds = await getMenuIds(supabase, franchiseId);\n\tconst activeAssignments = await getActiveVisibilityAssignments(\n\t\tsupabase,\n\t\tfranchiseId,\n\t\tplatform,\n\t);\n\n\tif (activeAssignments.length === 0) {\n\t\treturn emptyMenuExclusions();\n\t}\n\n\tconst categoryEffects = buildTargetEffectMap(\n\t\tactiveAssignments,\n\t\t(assignment) => assignment.category_id,\n\t);\n\tconst baseItemEffects = buildTargetEffectMap(\n\t\tactiveAssignments,\n\t\t(assignment) => assignment.base_item_id,\n\t);\n\tconst itemEffects = buildTargetEffectMap(\n\t\tactiveAssignments,\n\t\t(assignment) => assignment.item_id,\n\t);\n\n\tconst baseItemById = new Map(\n\t\tmenuIds.baseItems.map((baseItem) => [baseItem.base_item_id, baseItem]),\n\t);\n\tconst itemById = new Map(menuIds.items.map((item) => [item.item_id, item]));\n\n\tconst isCategoryIncluded = (categoryId: number) =>\n\t\tcategoryEffects.get(categoryId) !== \"exclude\";\n\n\tconst isBaseItemIncluded = (baseItemId: number) => {\n\t\tconst effect = baseItemEffects.get(baseItemId);\n\t\tif (effect === \"include\") return true;\n\t\tif (effect === \"exclude\") return false;\n\n\t\tconst categoryId = baseItemById.get(baseItemId)?.category_id;\n\t\treturn categoryId == null || isCategoryIncluded(categoryId);\n\t};\n\n\tconst isItemIncluded = (itemId: number): boolean => {\n\t\tconst effect = itemEffects.get(itemId);\n\t\tif (effect === \"include\") return true;\n\t\tif (effect === \"exclude\") return false;\n\n\t\tconst item = itemById.get(itemId);\n\t\tif (item?.base_item_id == null) return true;\n\n\t\treturn isBaseItemIncluded(item.base_item_id);\n\t};\n\n\tconst includedItems = new Set(\n\t\tmenuIds.items\n\t\t\t.filter((item) => isItemIncluded(item.item_id))\n\t\t\t.map((item) => item.item_id),\n\t);\n\tconst includedBaseItems = new Set(\n\t\tmenuIds.baseItems\n\t\t\t.filter((baseItem) => isBaseItemIncluded(baseItem.base_item_id))\n\t\t\t.map((baseItem) => baseItem.base_item_id),\n\t);\n\n\tfor (const item of menuIds.items) {\n\t\tif (includedItems.has(item.item_id) && item.base_item_id != null) {\n\t\t\tincludedBaseItems.add(item.base_item_id);\n\t\t}\n\t}\n\n\tconst includedCategories = new Set(\n\t\tmenuIds.categories\n\t\t\t.filter((category) => isCategoryIncluded(category.category_id))\n\t\t\t.map((category) => category.category_id),\n\t);\n\n\tfor (const baseItem of menuIds.baseItems) {\n\t\tif (includedBaseItems.has(baseItem.base_item_id)) {\n\t\t\tincludedCategories.add(baseItem.category_id);\n\t\t}\n\t}\n\n\treturn {\n\t\tcategoryIds: menuIds.categories\n\t\t\t.map((category) => category.category_id)\n\t\t\t.filter((categoryId) => !includedCategories.has(categoryId)),\n\t\tbaseItemIds: menuIds.baseItems\n\t\t\t.map((baseItem) => baseItem.base_item_id)\n\t\t\t.filter((baseItemId) => !includedBaseItems.has(baseItemId)),\n\t\titemIds: menuIds.items\n\t\t\t.map((item) => item.item_id)\n\t\t\t.filter((itemId) => !includedItems.has(itemId)),\n\t};\n}\n\nexport function getNameFromId(\n\tid: number,\n\ttype: \"category\" | \"item\" | \"item_variant\",\n\tmenu: Partial<Menu>,\n): string {\n\tif (type === \"category\") {\n\t\tconst category = menu.categories?.find(\n\t\t\t(category) => category.category_id === id,\n\t\t);\n\n\t\tif (category?.name) {\n\t\t\treturn category.name;\n\t\t}\n\t} else if (type === \"item\") {\n\t\tconst item = menu.items?.find((item: any) => item.base_item_id === id);\n\n\t\tif (item?.name) {\n\t\t\treturn item.name;\n\t\t}\n\t} else if (type === \"item_variant\") {\n\t\tconst item = menu.items?.find((item: any) =>\n\t\t\titem.variants.find((variant: any) => variant.item_id === id),\n\t\t);\n\n\t\tif (item?.name) {\n\t\t\treturn item.name;\n\t\t}\n\t}\n\n\treturn id.toString(); // default to showing the item id\n}\n\nfunction emptyMenuExclusions(): MenuIds {\n\treturn {\n\t\tcategoryIds: [],\n\t\tbaseItemIds: [],\n\t\titemIds: [],\n\t};\n}\n\nasync function getMenuIds(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n): Promise<MenuIdRows> {\n\tconst { data: categories, error: categoriesError } = await supabase\n\t\t.from(\"categories\")\n\t\t.select(\"category_id\")\n\t\t.eq(\"franchise_id\", franchiseId);\n\n\tif (categoriesError) {\n\t\tthrow categoriesError;\n\t}\n\n\tconst { data: baseItems, error: baseItemsError } = await supabase\n\t\t.from(\"base_items\")\n\t\t.select(\"base_item_id, category_id\")\n\t\t.eq(\"franchise_id\", franchiseId);\n\n\tif (baseItemsError) {\n\t\tthrow baseItemsError;\n\t}\n\n\tconst baseItemIds = (baseItems ?? []).map((item) => item.base_item_id);\n\tconst { data: items, error: itemsError } =\n\t\tbaseItemIds.length > 0\n\t\t\t? await supabase\n\t\t\t\t.from(\"items\")\n\t\t\t\t.select(\"item_id, base_item_id\")\n\t\t\t\t.in(\"base_item_id\", baseItemIds)\n\t\t\t: { data: [], error: null };\n\n\tif (itemsError) {\n\t\tthrow itemsError;\n\t}\n\n\treturn {\n\t\tcategories: categories ?? [],\n\t\tbaseItems: baseItems ?? [],\n\t\titems: items ?? [],\n\t};\n}\n\nasync function getActiveVisibilityAssignments(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n\tplatform: Platforms,\n): Promise<VisibilityConditionGroupAssignment[]> {\n\tconst { data, error } = await supabase\n\t\t.from(\"visibility_condition_group_assignments\")\n\t\t.select(\"*, visibility_condition_groups (*)\")\n\t\t.eq(\"visibility_condition_groups.franchise_id\", franchiseId)\n\t\t.eq(\"visibility_condition_groups.platform\", platform);\n\n\tif (error || !data) {\n\t\treturn [];\n\t}\n\n\tconst context = buildVisibilityEvaluationContext({ platform });\n\n\treturn (data as VisibilityConditionGroupAssignment[]).filter((assignment) => {\n\t\tconst group = assignment.visibility_condition_groups;\n\n\t\tif (!group?.condition || group.platform !== context.platform) {\n\t\t\treturn false;\n\t\t}\n\n\t\ttry {\n\t\t\treturn evaluateVisibilityCondition(group.condition, context);\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t});\n}\n\nfunction buildTargetEffectMap(\n\tassignments: VisibilityConditionGroupAssignment[],\n\tselector: (\n\t\tassignment: VisibilityConditionGroupAssignment,\n\t) => number | null | undefined,\n): Map<number, VisibilityEffect> {\n\tconst effectsByTarget = new Map<number, VisibilityEffect[]>();\n\n\tfor (const assignment of assignments) {\n\t\tconst targetId = selector(assignment);\n\t\tif (targetId == null) continue;\n\n\t\teffectsByTarget.set(targetId, [\n\t\t\t...(effectsByTarget.get(targetId) ?? []),\n\t\t\tassignment.effect,\n\t\t]);\n\t}\n\n\treturn new Map(\n\t\tArray.from(effectsByTarget.entries()).map(([targetId, effects]) => [\n\t\t\ttargetId,\n\t\t\teffects.includes(\"exclude\") ? \"exclude\" : \"include\",\n\t\t]),\n\t);\n}\n\nasync function getCategories(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n\texcludedIds?: number[],\n): Promise<Category[]> {\n\tlet categoriesQuery = supabase\n\t\t.from(\"categories\")\n\t\t.select(\"*\")\n\t\t.eq(\"franchise_id\", franchiseId)\n\t\t.order(\"sort_index\", ORDER_CURATED)\n\t\t.order(\"name\", ORDER_CURATED);\n\n\tif (excludedIds && excludedIds.length > 0) {\n\t\tcategoriesQuery = categoriesQuery.not(\n\t\t\t\"category_id\",\n\t\t\t\"in\",\n\t\t\tcsvFormat(excludedIds),\n\t\t);\n\t}\n\n\tconst { data: categories, error: categoriesError } = await categoriesQuery;\n\n\tif (categoriesError) {\n\t\tthrow categoriesError;\n\t}\n\n\treturn categories ?? [];\n}\n\nasync function getBaseItems(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n\texcludedIds?: number[],\n\tcategories?: Category[],\n): Promise<BaseItem[]> {\n\tif (categories && categories.length === 0) {\n\t\treturn [];\n\t}\n\n\tlet baseItemsQuery = supabase\n\t\t.from(\"base_items\")\n\t\t.select(\"*\")\n\t\t.eq(\"franchise_id\", franchiseId)\n\t\t.order(\"sort_index\", ORDER_CURATED)\n\t\t.order(\"name\", ORDER_CURATED);\n\n\tif (excludedIds && excludedIds.length > 0) {\n\t\tbaseItemsQuery = baseItemsQuery.not(\n\t\t\t\"base_item_id\",\n\t\t\t\"in\",\n\t\t\tcsvFormat(excludedIds),\n\t\t);\n\t}\n\n\tif (categories) {\n\t\tbaseItemsQuery = baseItemsQuery.in(\n\t\t\t\"category_id\",\n\t\t\tcategories.map((category) => category.category_id),\n\t\t);\n\t}\n\n\tconst { data: baseItems, error: baseItemsError } = await baseItemsQuery;\n\n\tif (baseItemsError) {\n\t\tthrow baseItemsError;\n\t}\n\n\tif (!baseItems || baseItems.length === 0) {\n\t\treturn [];\n\t}\n\n\tconst { data: images, error: imagesError } = await supabase\n\t\t.from(\"base_item_images\")\n\t\t.select(\"*\")\n\t\t.in(\n\t\t\t\"base_item_id\",\n\t\t\tbaseItems.map((baseItem) => baseItem.base_item_id),\n\t\t)\n\t\t.order(\"sort_index\", { ascending: true })\n\t\t.order(\"image_id\", { ascending: true });\n\n\tif (imagesError) {\n\t\tthrow imagesError;\n\t}\n\n\treturn attachImages(baseItems, images ?? []);\n}\n\n/**\n * Group images onto their base items. `images` must already be in the order\n * callers should see them (sort_index, then image_id) — grouping preserves it,\n * so every render site can take `images[0]` as the primary image.\n */\nexport function attachImages<T extends { base_item_id: number }>(\n\tbaseItems: T[],\n\timages: BaseItemImage[],\n): Array<T & { images: BaseItemImage[] }> {\n\tconst byBaseItemId = new Map<number, BaseItemImage[]>();\n\tfor (const image of images) {\n\t\tconst group = byBaseItemId.get(image.base_item_id);\n\t\tif (group) group.push(image);\n\t\telse byBaseItemId.set(image.base_item_id, [image]);\n\t}\n\n\treturn baseItems.map((baseItem) => ({\n\t\t...baseItem,\n\t\timages: byBaseItemId.get(baseItem.base_item_id) ?? [],\n\t}));\n}\n\nexport function menuImageUrl(\n\timage?: BaseItemImage | null,\n\tabsolute = false,\n): string | null {\n\tif (!image) return null;\n\tif (image.local_key && !absolute) return `/images/items/${image.local_key}`;\n\tconst path = image.processed_path ?? image.raw_path;\n\tif (!path) return null;\n\treturn `${SUPABASE_URL}/storage/v1/object/public/product-images/${path}`;\n}\n\nasync function getItems(\n\tsupabase: SupabaseClient,\n\tbaseItems: BaseItem[],\n\texcludedIds?: number[],\n): Promise<ItemVariant[]> {\n\tif (baseItems && baseItems.length === 0) {\n\t\treturn [];\n\t}\n\n\t// the scheme rides along so the prebuilt menu carries it — no runtime fetch\n\t// on the item page just to know how a pizza can be cut\n\t//\n\t// Price, not name, is the tiebreak: variants are usually sizes, and sorting\n\t// those alphabetically reads \"Large, Medium, Small\".\n\tlet itemsQuery = supabase\n\t\t.from(\"items\")\n\t\t.select(\"*, portion_scheme:portion_schemes(*)\")\n\t\t.in(\n\t\t\t\"base_item_id\",\n\t\t\tbaseItems.map((baseItem) => baseItem.base_item_id),\n\t\t)\n\t\t.order(\"sort_index\", ORDER_CURATED)\n\t\t.order(\"price\", ORDER_CURATED);\n\n\tif (excludedIds && excludedIds.length > 0) {\n\t\titemsQuery = itemsQuery.not(\"item_id\", \"in\", csvFormat(excludedIds));\n\t}\n\n\tconst { data: items, error: itemsError } = await itemsQuery;\n\n\tif (itemsError) {\n\t\tthrow itemsError;\n\t}\n\n\treturn items ?? [];\n}\n\nexport async function getComponents(\n\tsupabase: SupabaseClient,\n\titems: ItemVariant[] | number[],\n\tuseBehaviors = false,\n): Promise<Component[]> {\n\tconst itemIds = items.length > 0 && typeof items[0] === \"number\"\n\t\t? (items as number[])\n\t\t: (items as ItemVariant[]).map((item) => item.item_id);\n\n\tif (itemIds.length === 0) {\n\t\treturn [];\n\t}\n\n\tconst select = useBehaviors\n\t\t? \"components(*), item_id, servings_per_item, behavior_id, component_quantity_behaviors(headcount_threshold, starting_amount, step_size, step_interval, maximum)\"\n\t\t: \"components(*), item_id, servings_per_item\";\n\n\tconst { data: components } = await supabase\n\t\t.from(\"item_components\")\n\t\t.select(select)\n\t\t.in(\"item_id\", itemIds)\n\t\t.throwOnError();\n\n\treturn components.map((component: any) => ({\n\t\tservings_per_item: component.servings_per_item,\n\t\tis_base: component.components?.is_base,\n\t\tserving_weight: component.components?.serving_weight,\n\t\tcomponent_type_id: component.components?.component_type_id,\n\t\tid: component.components?.id,\n\t\tname: component.components?.name,\n\t\titem_id: component.item_id,\n\t\t...(useBehaviors && { behavior: component.component_quantity_behaviors ?? undefined }),\n\t}));\n}\n\n/**\n * A rule's options — components and items — as one keyed list. The key is what\n * `portionKey` builds on and what `weightedUpcharges` prices, so the picker and\n * the server's repricing read option prices through this same function.\n *\n * The two tables share one `sort_index` sequence, so a rule that mixes them\n * interleaves rather than always putting every component ahead of every item.\n */\nexport function ruleOptionDefs(rule: Rule) {\n\treturn [\n\t\t...(rule.components ?? []).map((c) => ({\n\t\t\tkey: `component:${c.component_id}`,\n\t\t\tid: c.component_id,\n\t\t\ttype: \"component\" as const,\n\t\t\tname: c.name,\n\t\t\tbasePrice: c.upcharge_price || 0,\n\t\t\tdescription: c.description,\n\t\t\tsort_index: c.sort_index ?? null,\n\t\t})),\n\t\t...(rule.items ?? []).map((i) => ({\n\t\t\tkey: `item:${i.item_id}`,\n\t\t\tid: i.item_id,\n\t\t\ttype: \"item\" as const,\n\t\t\tname: i.name,\n\t\t\tbasePrice: i.upcharge_price || 0,\n\t\t\tdescription: i.description,\n\t\t\tsort_index: i.sort_index ?? null,\n\t\t})),\n\t].sort(byCuratedThenName);\n}\n\n/**\n * Rules attach at item, base-item, or category level; `ruleIds` fetches them by\n * primary key instead, for when you already hold the ids (repricing an order's\n * selections server-side).\n */\nexport async function getRules(\n\tsupabase: SupabaseClient,\n\tbaseItems?: BaseItem[],\n\tcategories?: Category[],\n\titems?: ItemVariant[],\n\tuseBehaviors = false,\n\truleIds?: number[],\n): Promise<Rule[]> {\n\tconst itemIds = items?.map((item) => item.item_id) ?? [];\n\tconst baseItemIds = baseItems?.map((baseItem) => baseItem.base_item_id) ?? [];\n\tconst categoryIds = categories?.map((category) => category.category_id) ?? [];\n\n\tif (\n\t\titemIds.length === 0 &&\n\t\tbaseItemIds.length === 0 &&\n\t\tcategoryIds.length === 0 &&\n\t\t!ruleIds?.length\n\t) {\n\t\treturn [];\n\t}\n\n\tconst orFilter = [\n\t\t`item_id.in.(${itemIds.length > 0 ? itemIds.join(\",\") : -1})`,\n\t\t`base_item_id.in.(${baseItemIds.length > 0 ? baseItemIds.join(\",\") : -1})`,\n\t\t`category_id.in.(${categoryIds.length > 0 ? categoryIds.join(\",\") : -1})`,\n\t\t...(ruleIds?.length ? [`rule_id.in.(${ruleIds.join(\",\")})`] : []),\n\t].join(\",\");\n\n\tconst behaviorSelect = useBehaviors\n\t\t? \"component_quantity_behaviors(headcount_threshold, starting_amount, step_size, step_interval, maximum)\"\n\t\t: null;\n\n\tconst ruleComponentsSelect = [\n\t\t\"component_id, upcharge_price, sort_index, components(name, description, serving_weight)\",\n\t\tbehaviorSelect,\n\t].filter(Boolean).join(\", \");\n\n\tconst ruleItemsSelect = [\n\t\t\"item_id, upcharge_price, sort_index, items(item_id, name, description)\",\n\t\tbehaviorSelect,\n\t].filter(Boolean).join(\", \");\n\n\tconst { data: ruleData, error: rulesError } = await supabase\n\t\t.from(\"item_rules\")\n\t\t.select(\n\t\t\t`\n rule_id,\n category_id,\n item_id,\n base_item_id,\n group_name,\n min_selection,\n max_selection,\n included_quantity,\n is_portionable,\n sort_index,\n rule_components (${ruleComponentsSelect}),\n rule_items (${ruleItemsSelect})\n `,\n\t\t)\n\t\t.or(orFilter)\n\t\t// rule_id, not group_name: alphabetical rule groups (\"Choose a protein\"\n\t\t// before \"Select two sides\") is noise, so an unpinned rule falls back to\n\t\t// the order it was created in.\n\t\t.order(\"sort_index\", ORDER_CURATED)\n\t\t.order(\"rule_id\", ORDER_CURATED);\n\n\tif (rulesError) {\n\t\tthrow rulesError;\n\t}\n\n\treturn (ruleData ?? []).map((rule: any) => ({\n\t\trule_id: rule.rule_id,\n\t\tgroup_name: rule.group_name,\n\t\tmin_selection: rule.min_selection,\n\t\tmax_selection: rule.max_selection,\n\t\tincluded_quantity: rule.included_quantity,\n\t\tis_portionable: rule.is_portionable,\n\t\tcategory_id: rule.category_id,\n\t\titem_id: rule.item_id,\n\t\tbase_item_id: rule.base_item_id,\n\t\tsort_index: rule.sort_index ?? null,\n\n\t\t// Sorted here rather than in the query because the display name lives on\n\t\t// the joined components/items row. Every caller that renders these arrays\n\t\t// straight — the legacy customization modals, a site's server-rendered\n\t\t// item page — inherits the order without touching its own code.\n\t\tcomponents: (rule.rule_components ?? [])\n\t\t\t.map((component: any) => ({\n\t\t\t\tcomponent_id: component.component_id,\n\t\t\t\tupcharge_price: component.upcharge_price ?? 0,\n\t\t\t\tname: component.components?.name ?? \"\",\n\t\t\t\tdescription: component.components?.description ?? undefined,\n\t\t\t\tserving_weight: component.components?.serving_weight ?? undefined,\n\t\t\t\tsort_index: component.sort_index ?? null,\n\t\t\t\t...(useBehaviors && { behavior: component.component_quantity_behaviors ?? undefined }),\n\t\t\t}))\n\t\t\t.sort(byCuratedThenName),\n\n\t\titems: (rule.rule_items ?? [])\n\t\t\t.map((item: any) => ({\n\t\t\t\titem_id: item.item_id,\n\t\t\t\tname: item.items?.name ?? \"\",\n\t\t\t\tdescription: item.items?.description ?? undefined,\n\t\t\t\tupcharge_price: item.upcharge_price ?? 0,\n\t\t\t\tsort_index: item.sort_index ?? null,\n\t\t\t\t...(useBehaviors && { behavior: item.component_quantity_behaviors ?? undefined }),\n\t\t\t}))\n\t\t\t.sort(byCuratedThenName),\n\t}));\n}\n\nfunction buildMenu(\n\titems: ItemVariant[],\n\tbaseItems: BaseItem[],\n\tcategories: Category[],\n\tcomponents: Component[],\n\trules: Rule[],\n): Menu {\n\tconst menu: Menu = {\n\t\titems: [],\n\t\tcategories: [],\n\t\trules,\n\t\tcomponents,\n\t};\n\n\tmenu.items = baseItems.map((baseItem) => {\n\t\tconst categoryRules = rules.filter((rule) => rule.category_id === baseItem.category_id);\n\t\tconst baseItemRules = rules.filter((rule) => rule.base_item_id === baseItem.base_item_id);\n\t\tconst category = categories.find((c) => c.category_id === baseItem.category_id);\n\t\treturn {\n\t\t\t...baseItem,\n\t\t\tis_catering: category?.is_catering ?? false,\n\t\t\tvariants: items\n\t\t\t\t.filter((item) => item.base_item_id === baseItem.base_item_id)\n\t\t\t\t.map((item) => ({\n\t\t\t\t\t...item,\n\t\t\t\t\tcomponents: components.filter(\n\t\t\t\t\t\t(component) => component.item_id === item.item_id,\n\t\t\t\t\t),\n\t\t\t\t\trules: [\n\t\t\t\t\t\t...categoryRules,\n\t\t\t\t\t\t...baseItemRules,\n\t\t\t\t\t\t...rules.filter((rule) => rule.item_id === item.item_id),\n\t\t\t\t\t],\n\t\t\t\t})),\n\t\t\trules: baseItemRules,\n\t\t};\n\t});\n\n\tmenu.categories = categories.map((category) => ({\n\t\t...category,\n\t\trules: rules.filter((rule) => rule.category_id === category.category_id),\n\t}));\n\n\treturn menu;\n}\n\nexport function getBaseItemFromVariant(\n\tmenu: MenuItem[],\n\tvariant: ItemVariant,\n\tbaseItemId?: number,\n): MenuItem | undefined {\n\tif (baseItemId != null) {\n\t\treturn menu.find((i) => i.base_item_id === baseItemId);\n\t}\n\treturn menu.find((i) => i.variants.some((v) => v.item_id === variant.item_id));\n}\n\nexport function getMenuItemByVariantId(menu: MenuItem[], variantId: number): MenuItem | undefined {\n\treturn menu.find((item) =>\n\t\titem.variants?.some((v) => v.item_id === variantId)\n\t);\n}\n\nexport function filterBySchedule(\n\tpromos: Promotion[],\n\tplatform: Platforms,\n\tlocationId: number | undefined,\n\tnow: Date = new Date(),\n): Promotion[] {\n\treturn promos.filter((promo) => {\n\t\tif (promo.start_date && new Date(promo.start_date) > now) return false;\n\t\tif (promo.end_date && new Date(promo.end_date) < now) return false;\n\n\t\tif (promo.platforms && promo.platforms.length > 0) {\n\t\t\tif (!promo.platforms.includes(platform)) return false;\n\t\t}\n\n\t\tif (!promo.is_automatic) return false;\n\n\t\tif (promo.location_id != null && locationId !== undefined) {\n\t\t\tif (promo.location_id !== locationId) return false;\n\t\t} else if (promo.location_id != null && locationId === undefined) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif (promo.schedule_details_json) {\n\t\t\tconst { interval, days, start_time, end_time } =\n\t\t\t\tpromo.schedule_details_json;\n\n\t\t\tif (interval === \"weekly\" && days && !days.includes(getDay(now) + 1)) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst dayStart = startOfDay(now);\n\t\t\tif (start_time && isBefore(now, parse(start_time, \"HH:mm\", dayStart))) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (end_time && isAfter(now, parse(end_time, \"HH:mm\", dayStart))) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t});\n}\n\nexport async function getActivePromotions(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n\tplatform: Platforms,\n\tlocationId?: number,\n): Promise<Promotion[]> {\n\tconst { data, error } = await supabase\n\t\t.from(\"promotions\")\n\t\t.select(\"*\")\n\t\t.eq(\"is_active\", true)\n\t\t.eq(\"franchise_id\", franchiseId);\n\n\tif (error) throw error;\n\n\treturn filterBySchedule((data || []) as Promotion[], platform, locationId);\n}\n","export { menuImageUrl } from \"@zorgo/universal/utils/menuUtils\";\nexport type { BaseItemImage } from \"@zorgo/universal/interfaces\";\n\n/**\n * The variant widths prebuild emits, for menu item images and site-authored\n * photos alike. The loader below and the encoder in internal/images.ts read the\n * same list, so a requested width can never snap to a rung that was never\n * written. 1920 is the top rung because a full-bleed hero on a retina laptop\n * asks for it; `withoutEnlargement` means a smaller source just stops early.\n */\nexport const IMAGE_WIDTHS = [384, 640, 1080, 1920] as const;\n\n/** @deprecated The ladder is no longer item-only — use {@link IMAGE_WIDTHS}. */\nexport const ITEM_IMAGE_WIDTHS = IMAGE_WIDTHS;\n\n/** Public prefix for everything prebuild encodes. */\nexport const IMAGE_PREFIX = \"/images/\";\n\n/** Public prefix menuImageUrl returns for a localized item image. */\nexport const ITEM_IMAGE_PREFIX = \"/images/items/\";\n\n/**\n * next/image custom loader (`images.loaderFile` in a scaffolded site's\n * next.config.ts).\n *\n * An extension-less path under `/images/` is prebuild's contract for \"this has\n * a variant ladder\" — it gets the nearest rung that covers the requested width.\n * That covers both menu item images (`/images/items/<key>`) and the site's own\n * photos (`/images/food`, encoded from `assets/images/food.JPG`).\n *\n * Everything else passes through untouched and is served as-is: a bucket URL\n * for an item prebuild could not localize, the prebuilt logo, a video poster —\n * anything that still carries a file extension.\n *\n * Sync and pure by contract: next/image calls it during render for every srcset\n * candidate.\n */\nexport default function imageLoader({\n\tsrc,\n\twidth,\n}: {\n\tsrc: string;\n\twidth: number;\n}): string {\n\tif (!src.startsWith(IMAGE_PREFIX) || /\\.[a-z0-9]+$/i.test(src)) return src;\n\tconst rung =\n\t\tIMAGE_WIDTHS.find((candidate) => candidate >= width) ??\n\t\tIMAGE_WIDTHS[IMAGE_WIDTHS.length - 1];\n\treturn `${src}-${rung}.avif`;\n}\n\nexport { imageLoader, imageLoader as itemImageLoader };\n"],"mappings":"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","names":["import_fs","import_path","menu","locations","path","path","path","menu","path","menu"]}