diff --git a/CLAUDE.md b/CLAUDE.md index 579de0e2..8c98d998 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,1316 +1,213 @@ -## Claude Guidelines for this Project - ---- +# Claude Guidelines for CannaiQ ## PERMANENT RULES (NEVER VIOLATE) -### 1. NO DELETION OF DATA — EVER +### 1. NO DELETE +Never delete data, files, images, logs, or database rows. CannaiQ is a historical analytics system. -CannaiQ is a **historical analytics system**. Data retention is **permanent by design**. +### 2. NO KILL +Never run `pkill`, `kill`, `killall`, or similar. Say "Please run `./stop-local.sh`" instead. -**NEVER delete:** -- Product records -- Crawled snapshots -- Images -- Directories -- Logs -- Orchestrator traces -- Profiles -- Selector configs -- Crawl outcomes -- Store data -- Brand data +### 3. NO MANUAL STARTUP +Never start servers manually. Say "Please run `./setup-local.sh`" instead. -**NEVER automate cleanup:** -- No cron or scheduled job may `rm`, `unlink`, `delete`, `purge`, `prune`, `clean`, or `reset` any storage directory or DB row -- No migration may DELETE data — only add/update/alter columns -- If cleanup is required, ONLY the user may issue a manual command +### 4. DEPLOYMENT AUTH REQUIRED +Never deploy unless user explicitly says: "CLAUDE — DEPLOYMENT IS NOW AUTHORIZED." -**Code enforcement:** -- `local-storage.ts` must only: write files, create directories, read files -- No `deleteImage`, `deleteProductImages`, or similar functions - -### 2. NO PROCESS KILLING — EVER - -**Claude must NEVER run process-killing commands:** -- No `pkill` -- No `kill -9` -- No `xargs kill` -- No `lsof | kill` -- No `killall` -- No `fuser -k` - -**Claude must NOT manage host processes.** Only user scripts manage the local environment. - -**Correct behavior:** -- If backend is running on port 3010 → say: "Backend already running" -- If backend is NOT running → say: "Please run `./setup-local.sh`" - -**Process management is done ONLY by user scripts:** -```bash -./setup-local.sh # Start local environment -./stop-local.sh # Stop local environment -``` - -### 3. NO MANUAL SERVER STARTUP — EVER - -**Claude must NEVER start the backend manually:** -- No `npx tsx src/index.ts` -- No `node dist/index.js` -- No `npm run dev` with custom env vars -- No `DATABASE_URL=... npx tsx ...` - -**Claude must NEVER set DATABASE_URL in shell commands:** -- DB connection uses `CANNAIQ_DB_*` env vars or `CANNAIQ_DB_URL` from the user's environment -- Never hardcode connection strings in bash commands -- Never override env vars to bypass the user's DB setup - -**If backend is not running:** -- Say: "Please run `./setup-local.sh`" -- Do NOT attempt to start it yourself - -**If a dependency is missing:** -- Add it to `package.json` -- Say: "Please run `cd backend && npm install`" -- Do NOT try to solve it by starting a custom dev server - -**The ONLY way to start local services:** -```bash -cd backend -./setup-local.sh -``` - -### 4. DEPLOYMENT AUTHORIZATION REQUIRED - -**NEVER deploy to production unless the user explicitly says:** -> "CLAUDE — DEPLOYMENT IS NOW AUTHORIZED." - -Until then: -- All work is LOCAL ONLY -- No `kubectl apply`, `docker push`, or remote operations -- No port-forwarding to production -- No connecting to Kubernetes clusters - -### 5. DATABASE CONNECTION ARCHITECTURE - -**Migration code is CLI-only. Runtime code must NOT import `src/db/migrate.ts`.** - -| Module | Purpose | Import From | -|--------|---------|-------------| -| `src/db/migrate.ts` | CLI migrations only | **NEVER import at runtime** | -| `src/db/pool.ts` | Runtime database pool | `import { pool } from '../db/pool'` | -| `src/dutchie-az/db/connection.ts` | Canonical connection helper | Alternative for runtime | - -**Runtime gets DB connections ONLY via:** -```typescript -import { pool } from '../db/pool'; -// or -import { getPool } from '../dutchie-az/db/connection'; -``` - -**To run migrations:** -```bash -cd backend -npx tsx src/db/migrate.ts -``` - -**Why this matters:** -- `migrate.ts` validates env vars strictly and throws at module load time -- Importing it at runtime causes startup crashes if env vars aren't perfect -- `pool.ts` uses lazy initialization - only validates when first query is made - -### 6. ALL API ROUTES REQUIRE AUTHENTICATION — NO EXCEPTIONS - -**Every API router MUST apply `authMiddleware` at the router level.** - -```typescript -import { authMiddleware } from '../auth/middleware'; - -const router = Router(); -router.use(authMiddleware); // REQUIRED - first line after router creation -``` - -**Authentication flow (see `src/auth/middleware.ts`):** -1. Check Bearer token (JWT or API token) → grant access if valid -2. Check trusted origins (cannaiq.co, findadispo.com, localhost, etc.) → grant access -3. Check trusted IPs (127.0.0.1, ::1, internal pod IPs) → grant access -4. **Return 401 Unauthorized** if none of the above - -**NEVER create API routes without auth middleware:** -- No "public" endpoints that bypass authentication -- No "read-only" exceptions -- No "analytics-only" exceptions -- If an endpoint exists under `/api/*`, it MUST be protected - -**When creating new route files:** -1. Import `authMiddleware` from `../auth/middleware` -2. Add `router.use(authMiddleware)` immediately after creating the router -3. Document security requirements in file header comments - -**Trusted origins (defined in middleware):** -- `https://cannaiq.co` -- `https://findadispo.com` -- `https://findagram.co` -- `*.cannabrands.app` domains -- `localhost:*` for development - -### 7. LOCAL DEVELOPMENT BY DEFAULT - -**Quick Start:** -```bash -./setup-local.sh -``` - -**Services (all started by setup-local.sh):** -| Service | URL | Purpose | -|---------|-----|---------| -| PostgreSQL | localhost:54320 | cannaiq-postgres container | -| Backend API | http://localhost:3010 | Express API server | -| CannaiQ Admin | http://localhost:8080/admin | B2B admin dashboard | -| FindADispo | http://localhost:3001 | Consumer dispensary finder | -| Findagram | http://localhost:3002 | Consumer delivery marketplace | - -**In local mode:** -- Use `docker-compose.local.yml` (NO MinIO) -- Use local filesystem storage at `./storage` -- Connect to `cannaiq-postgres` at `localhost:54320` -- Backend runs at `localhost:3010` -- All three frontends run on separate ports (8080, 3001, 3002) -- NO remote connections, NO Kubernetes, NO MinIO - -**Environment:** -- All DB config is in `backend/.env` -- STORAGE_DRIVER=local -- STORAGE_BASE_PATH=./storage - -**Local Admin Bootstrap:** -```bash -cd backend -npx tsx src/scripts/bootstrap-local-admin.ts -``` - -Creates/resets a deterministic local admin user: -| Field | Value | -|-------|-------| -| Email | `admin@local.test` | -| Password | `admin123` | -| Role | `superadmin` | - -This is a LOCAL-DEV helper only. Never use these credentials in production. - -**Manual startup (if not using setup-local.sh):** -```bash -# Terminal 1: Start PostgreSQL -docker-compose -f docker-compose.local.yml up -d - -# Terminal 2: Start Backend -cd backend && npm run dev - -# Terminal 3: Start Frontend -cd cannaiq && npm run dev:admin -``` - -**Stop services:** -```bash -./stop-local.sh -``` +### 5. DB POOL ONLY +Never import `src/db/migrate.ts` at runtime. Use `src/db/pool.ts` for DB access. --- -## DATABASE MODEL (CRITICAL) +## Quick Reference -### Database Architecture +### Database Tables +| USE THIS | NOT THIS | +|----------|----------| +| `dispensaries` | `stores` (empty) | +| `store_products` | `products` (empty) | +| `store_product_snapshots` | `dutchie_product_snapshots` | -CannaiQ has **TWO databases** with distinct purposes: +### Key Files +| Purpose | File | +|---------|------| +| Dutchie client | `src/platforms/dutchie/client.ts` | +| DB pool | `src/db/pool.ts` | +| Payload fetch | `src/tasks/handlers/payload-fetch.ts` | +| Product refresh | `src/tasks/handlers/product-refresh.ts` | -| Database | Purpose | Access | -|----------|---------|--------| -| `dutchie_menus` | **Canonical CannaiQ database** - All schema, migrations, and application data | READ/WRITE | -| `dutchie_legacy` | **Legacy read-only archive** - Historical data from old system | READ-ONLY | +### Dutchie GraphQL +- **Endpoint**: `https://dutchie.com/api-3/graphql` +- **Hash (FilteredProducts)**: `ee29c060826dc41c527e470e9ae502c9b2c169720faa0a9f5d25e1b9a530a4a0` +- **CRITICAL**: Use `Status: 'Active'` (not `null`) -### Store vs Dispensary Terminology - -**"Store" and "Dispensary" are SYNONYMS in CannaiQ.** - -| Term | Usage | DB Table | -|------|-------|----------| -| Store | API routes (`/api/stores`) | `dispensaries` | -| Dispensary | DB table, internal code | `dispensaries` | - -- `/api/stores` and `/api/dispensaries` both query the `dispensaries` table -- There is NO `stores` table in use - it's a legacy empty table -- Use these terms interchangeably in code and documentation - -### Canonical vs Legacy Tables - -**CANONICAL TABLES (USE THESE):** - -| Table | Purpose | Row Count | -|-------|---------|-----------| -| `dispensaries` | Store/dispensary records | ~188+ rows | -| `store_products` | Product catalog | ~37,000+ rows | -| `store_product_snapshots` | Price/stock history | ~millions | - -**LEGACY TABLES (EMPTY - DO NOT USE):** - -| Table | Status | Action | -|-------|--------|--------| -| `stores` | EMPTY (0 rows) | Use `dispensaries` instead | -| `products` | EMPTY (0 rows) | Use `store_products` instead | -| `dutchie_products` | LEGACY (0 rows) | Use `store_products` instead | -| `dutchie_product_snapshots` | LEGACY (0 rows) | Use `store_product_snapshots` instead | -| `categories` | EMPTY (0 rows) | Categories stored in product records | - -**Code must NEVER:** -- Query the `stores` table (use `dispensaries`) -- Query the `products` table (use `store_products`) -- Query the `dutchie_products` table (use `store_products`) -- Query the `categories` table (categories are in product records) - -**CRITICAL RULES:** -- **Migrations ONLY run on `dutchie_menus`** - NEVER on `dutchie_legacy` -- **Application code connects ONLY to `dutchie_menus`** -- **ETL scripts READ from `dutchie_legacy`, WRITE to `dutchie_menus`** -- `dutchie_legacy` is frozen - NO writes, NO schema changes, NO migrations - -### Environment Variables - -**CannaiQ Database (dutchie_menus) - PRIMARY:** -```bash -# All application/migration DB access uses these env vars: -CANNAIQ_DB_HOST=localhost # Database host -CANNAIQ_DB_PORT=54320 # Database port -CANNAIQ_DB_NAME=dutchie_menus # MUST be dutchie_menus -CANNAIQ_DB_USER=dutchie # Database user -CANNAIQ_DB_PASS= # Database password - -# OR use a full connection string: -CANNAIQ_DB_URL=postgresql://user:pass@host:port/dutchie_menus -``` - -**Legacy Database (dutchie_legacy) - ETL ONLY:** -```bash -# Only used by ETL scripts for reading legacy data: -LEGACY_DB_HOST=localhost -LEGACY_DB_PORT=54320 -LEGACY_DB_NAME=dutchie_legacy # READ-ONLY - never migrated -LEGACY_DB_USER=dutchie -LEGACY_DB_PASS= - -# OR use a full connection string: -LEGACY_DB_URL=postgresql://user:pass@host:port/dutchie_legacy -``` - -**Key Rules:** -- `CANNAIQ_DB_NAME` MUST be `dutchie_menus` for application/migrations -- `LEGACY_DB_NAME` is `dutchie_legacy` - READ-ONLY for ETL only -- ALL application code MUST use `CANNAIQ_DB_*` environment variables -- No hardcoded database names anywhere in the codebase -- `backend/.env` controls all database access for local development - -**State Modeling:** -- States (AZ, MI, CA, NV, etc.) are modeled via `states` table + `state_id` on dispensaries -- NO separate databases per state -- Use `state_code` or `state_id` columns for filtering - -### Migration and ETL Procedure - -**Step 1: Run schema migration (on dutchie_menus ONLY):** -```bash -cd backend -psql "postgresql://dutchie:dutchie_local_pass@localhost:54320/dutchie_menus" \ - -f migrations/041_cannaiq_canonical_schema.sql -``` - -**Step 2: Run ETL to copy legacy data:** -```bash -cd backend -npx tsx src/scripts/etl/042_legacy_import.ts -# Reads from dutchie_legacy, writes to dutchie_menus -``` - -### Database Access Rules - -**Claude MUST NOT:** -- Connect to any database besides the canonical CannaiQ database -- Use raw connection strings in shell commands -- Run `psql` commands directly -- Construct database URLs manually -- Create or rename databases automatically -- Run `npm run migrate` without explicit user authorization -- Patch schema at runtime (no ALTER TABLE from scripts) - -**All data access MUST go through:** -- LOCAL CannaiQ backend HTTP API endpoints -- Internal CannaiQ application code (using canonical connection pool) -- Ask user to run SQL manually if absolutely needed - -**Local service management:** -- User starts services via `./setup-local.sh` (ONLY the user runs this) -- If port 3010 responds, assume backend is running -- If port 3010 does NOT respond, tell user: "Backend is not running; please run `./setup-local.sh`" -- Claude may only access the app via HTTP: `http://localhost:3010` (API), `http://localhost:8080/admin` (UI) -- Never restart, kill, or manage local processes — that is the user's responsibility - -### Migrations - -**Rules:** -- Migrations may be WRITTEN but only the USER runs them after review -- Never execute migrations automatically -- Only additive migrations (no DROP/DELETE) -- Write schema-tolerant code that handles missing optional columns - -**If schema changes are needed:** -1. Generate a proper migration file in `backend/migrations/*.sql` -2. Show the migration to the user -3. Wait for explicit authorization before running -4. Never run migrations automatically - only the user runs them after review - -**Schema tolerance:** -- If a column is missing at runtime, prefer making the code tolerant (treat field as optional) instead of auto-creating the column -- Queries should gracefully handle missing columns by omitting them or using NULL defaults - -### Canonical Schema Migration (041/042) - -**Migration 041** (`backend/migrations/041_cannaiq_canonical_schema.sql`): -- Creates canonical CannaiQ tables: `states`, `chains`, `brands`, `store_products`, `store_product_snapshots`, `crawl_runs` -- Adds `state_id` and `chain_id` columns to `dispensaries` -- Adds status columns to `dispensary_crawler_profiles` -- SCHEMA ONLY - no data inserts from legacy tables - -**ETL Script 042** (`backend/src/scripts/etl/042_legacy_import.ts`): -- Copies data from legacy `dutchie_legacy.dutchie_products` → `store_products` -- Copies data from legacy `dutchie_legacy.dutchie_product_snapshots` → `store_product_snapshots` -- Extracts brands from product data into `brands` table -- Links dispensaries to chains and states -- INSERT-ONLY and IDEMPOTENT (uses ON CONFLICT DO NOTHING) -- Run manually: `cd backend && npx tsx src/scripts/etl/042_legacy_import.ts` - -**Tables touched by ETL:** -| Source Table (dutchie_legacy) | Target Table (dutchie_menus) | -|-------------------------------|------------------------------| -| `dutchie_products` | `store_products` | -| `dutchie_product_snapshots` | `store_product_snapshots` | -| (brand names extracted) | `brands` | -| (state codes mapped) | `dispensaries.state_id` | -| (chain names matched) | `dispensaries.chain_id` | - -**Note:** The legacy `dutchie_products` and `dutchie_product_snapshots` tables in `dutchie_legacy` are read-only sources. All new crawl data goes directly to `store_products` and `store_product_snapshots`. - -**Migration 045** (`backend/migrations/045_add_image_columns.sql`): -- Adds `thumbnail_url` to `store_products` and `store_product_snapshots` -- `image_url` already exists from migration 041 -- ETL 042 populates `image_url` from legacy `primary_image_url` where present -- `thumbnail_url` is NULL for legacy data - future crawls can populate it - -### Deprecated Connection Module - -The custom connection module at `src/dutchie-az/db/connection` is **DEPRECATED**. - -**All code using `getClient` from this module must be refactored to:** -- Use the CannaiQ API endpoints instead -- Use the orchestrator through the API -- Use the canonical DB pool from the main application +### Frontends +| Folder | Domain | Build | +|--------|--------|-------| +| `cannaiq/` | cannaiq.co | Vite | +| `findadispo/` | findadispo.com | CRA | +| `findagram/` | findagram.co | CRA | +| `frontend/` | DEPRECATED | - | --- -## PERFORMANCE REQUIREMENTS +## Deprecated Code -**Database Queries:** -- NEVER write N+1 queries - always batch fetch related data before iterating -- NEVER run queries inside loops - batch them before the loop -- Avoid multiple queries when one JOIN or subquery works -- Dashboard/index pages should use MAX 5-10 queries total, not 50+ -- Mentally trace query count - if a page would run 20+ queries, refactor -- Cache expensive aggregations (in-memory or Redis, 5-min TTL) instead of recalculating every request -- Use query logging during development to verify query count +**DO NOT USE** anything in `src/_deprecated/`: +- `hydration/` - Use `src/tasks/handlers/` +- `scraper-v2/` - Use `src/platforms/dutchie/` +- `canonical-hydration/` - Merged into tasks -**Before submitting route/controller code, verify:** -1. No queries inside `forEach`/`map`/`for` loops -2. All related data fetched in batches before iteration -3. Aggregations done in SQL (`COUNT`, `SUM`, `AVG`, `GROUP BY`), not in JS -4. **Would this cause a 503 under load? If unsure, simplify.** - -**Examples of BAD patterns:** -```typescript -// BAD: N+1 query - runs a query for each store -const stores = await getStores(); -for (const store of stores) { - store.products = await getProductsByStoreId(store.id); // N queries! -} - -// BAD: Query inside map -const results = await Promise.all( - storeIds.map(id => pool.query('SELECT * FROM products WHERE store_id = $1', [id])) -); -``` - -**Examples of GOOD patterns:** -```typescript -// GOOD: Batch fetch all products, then group in JS -const stores = await getStores(); -const storeIds = stores.map(s => s.id); -const allProducts = await pool.query( - 'SELECT * FROM products WHERE store_id = ANY($1)', [storeIds] -); -const productsByStore = groupBy(allProducts.rows, 'store_id'); -stores.forEach(s => s.products = productsByStore[s.id] || []); - -// GOOD: Single query with JOIN -const result = await pool.query(` - SELECT s.*, COUNT(p.id) as product_count - FROM stores s - LEFT JOIN products p ON p.store_id = s.id - GROUP BY s.id -`); -``` +**DO NOT USE** `src/dutchie-az/db/connection.ts` - Use `src/db/pool.ts` --- -## FORBIDDEN ACTIONS +## Local Development -1. **Deleting any data** (products, snapshots, images, logs, traces) -2. **Deploying without explicit authorization** -3. **Connecting to Kubernetes** without authorization -4. **Port-forwarding to production** without authorization -5. **Starting MinIO** in local development -6. **Using S3/MinIO SDKs** when `STORAGE_DRIVER=local` -7. **Automating cleanup** of any kind -8. **Dropping database tables or columns** -9. **Overwriting historical records** (always append snapshots) -10. **Runtime schema patching** (ALTER TABLE from scripts) -11. **Using `getClient` from deprecated connection module** -12. **Creating ad-hoc database connections** outside the canonical pool -13. **Auto-adding missing columns** at runtime -14. **Killing local processes** (`pkill`, `kill`, `kill -9`, etc.) -15. **Starting backend/frontend directly** with custom env vars -16. **Running `lsof -ti:PORT | xargs kill`** or similar process-killing commands -17. **Using hardcoded database names** in code or comments -18. **Creating or connecting to a second database** -19. **Creating API routes without authMiddleware** (all `/api/*` routes MUST be protected) - ---- - -## STORAGE BEHAVIOR - -### Local Storage Structure - -``` -/storage/images/products/{state}/{store}/{brand}/{product}/ - image-{hash}.webp - -/storage/images/brands/{brand}/ - logo-{hash}.webp -``` - -### Image Proxy API (On-Demand Resizing) - -Images are stored at full resolution and resized on-demand via the `/img` endpoint. - -**Endpoint:** `GET /img/?` - -**Parameters:** -| Param | Description | Example | -|-------|-------------|---------| -| `w` | Width in pixels (max 4000) | `?w=200` | -| `h` | Height in pixels (max 4000) | `?h=200` | -| `q` | Quality 1-100 (default 80) | `?q=70` | -| `fit` | Resize mode: cover, contain, fill, inside, outside | `?fit=cover` | -| `blur` | Blur sigma 0.3-1000 | `?blur=5` | -| `gray` | Grayscale (1 = enabled) | `?gray=1` | -| `format` | Output: webp, jpeg, png, avif (default webp) | `?format=jpeg` | - -**Examples:** ```bash -# Thumbnail (50px) -GET /img/products/az/store/brand/product/image-abc123.webp?w=50 - -# Card image (200px, cover fit) -GET /img/products/az/store/brand/product/image-abc123.webp?w=200&h=200&fit=cover - -# JPEG at 70% quality -GET /img/products/az/store/brand/product/image-abc123.webp?w=400&format=jpeg&q=70 - -# Grayscale blur -GET /img/products/az/store/brand/product/image-abc123.webp?w=200&gray=1&blur=3 +./setup-local.sh # Start all services +./stop-local.sh # Stop all services ``` -**Frontend Usage:** -```typescript -import { getImageUrl, ImageSizes } from '../lib/images'; +| Service | URL | +|---------|-----| +| API | http://localhost:3010 | +| Admin | http://localhost:8080/admin | +| PostgreSQL | localhost:54320 | -// Returns /img/products/.../image.webp?w=50 for local images -// Returns original URL for remote images (CDN, etc.) -const thumbUrl = getImageUrl(product.image_url, ImageSizes.thumb); -const cardUrl = getImageUrl(product.image_url, ImageSizes.medium); -const detailUrl = getImageUrl(product.image_url, ImageSizes.detail); -``` +--- -**Size Presets:** -| Preset | Width | Use Case | -|--------|-------|----------| -| `thumb` | 50px | Table thumbnails | -| `small` | 100px | Small cards | -| `medium` | 200px | Grid cards | -| `large` | 400px | Large cards | -| `detail` | 600px | Product detail | -| `full` | - | No resize | - -### Storage Adapter - -```typescript -import { saveImage, getImageUrl } from '../utils/storage-adapter'; - -// Automatically uses local storage when STORAGE_DRIVER=local -``` - -### Files +## WordPress Plugin (ACTIVE) +### Plugin Files | File | Purpose | |------|---------| -| `backend/src/utils/image-storage.ts` | Image download and storage | -| `backend/src/routes/image-proxy.ts` | On-demand image resizing endpoint | -| `cannaiq/src/lib/images.ts` | Frontend image URL helper | -| `docker-compose.local.yml` | Local stack without MinIO | -| `start-local.sh` | Convenience startup script | +| `wordpress-plugin/cannaiq-menus.php` | Main plugin (CannaIQ brand) | +| `wordpress-plugin/crawlsy-menus.php` | Legacy plugin (Crawlsy brand) | +| `wordpress-plugin/VERSION` | Version tracking | + +### API Routes (Backend) +- `GET /api/v1/wordpress/dispensaries` - List dispensaries +- `GET /api/v1/wordpress/dispensary/:id/menu` - Get menu data +- Route file: `backend/src/routes/wordpress.ts` + +### Versioning +Bump `wordpress-plugin/VERSION` on changes: +- Minor (x.x.N): bug fixes +- Middle (x.N.0): new features +- Major (N.0.0): breaking changes (user must request) --- -## UI ANONYMIZATION RULES +## Puppeteer Scraping (Browser-Based) -- No vendor names in forward-facing URLs -- No "dutchie", "treez", "jane", "weedmaps", "leafly" visible in consumer UIs -- Internal admin tools may show provider names for debugging +### Age Gate Bypass ---- +Most dispensary sites require age verification. The browser scraper handles this automatically: -## DUTCHIE DISCOVERY PIPELINE (Added 2025-01) +**Utility File**: `src/utils/age-gate.ts` -### Overview -Automated discovery of Dutchie-powered dispensaries across all US states. +**Key Functions**: +- `setAgeGateCookies(page, url, state)` - Set cookies BEFORE navigation to prevent gate +- `hasAgeGate(page)` - Detect if page shows age verification +- `bypassAgeGate(page, state)` - Click through age gate if displayed +- `detectStateFromUrl(url)` - Extract state from URL (e.g., `-az-` → Arizona) -### Flow -``` -1. getAllCitiesByState GraphQL → Get all cities for a state -2. ConsumerDispensaries GraphQL → Get stores for each city -3. Upsert to dutchie_discovery_locations (keyed by platform_location_id) -4. AUTO-VALIDATE: Check required fields -5. AUTO-PROMOTE: Create/update dispensaries with crawl_enabled=true -6. Log all actions to dutchie_promotion_log -``` +**Cookie Names Set**: +- `age_gate_passed: 'true'` +- `selected_state: ''` +- `age_verified: 'true'` -### Tables -| Table | Purpose | -|-------|---------| -| `dutchie_discovery_cities` | Cities known to have dispensaries | -| `dutchie_discovery_locations` | Raw discovered store data | -| `dispensaries` | Canonical stores (promoted from discovery) | -| `dutchie_promotion_log` | Audit trail for validation/promotion | +**Bypass Methods** (tried in order): +1. Custom dropdown (shadcn/radix style) - Curaleaf pattern +2. Standard `