Files
route-commerce/README.md
T
Tyler 11cd2fd01a
Deploy to route.crispygoat.com / deploy (push) Successful in 4m33s
Water Log: production-ready module (schema, auth, CRUD, UI, tests, docs)
Completes the Tuxedo Water Log feature end-to-end. Builds on the existing
water_* tables in 0001_init.sql; extends them with 0090 and rewrites the
server actions, settings page, export route, and admin panel to actually
function.

Schema (db/migrations/0090_water_log_completion.sql, db/schema/water-log.ts)
- headgate_token + status + max_flow_gpm + thresholds + notes on water_headgates
- role + phone + notes on water_irrigators
- method + total_gallons + photo_url + logged_date + lat/lon on water_log_entries
- new tables: water_admin_settings, water_admin_sessions, water_audit_log, water_alert_log

Auth (src/lib/water-log-pin.ts, src/lib/water-log-audit.ts)
- scrypt N=16384 per-PIN random salt, self-describing hash format
- weak-PIN detection (repeats, monotonic, palindromes)
- 200ms delay + timingSafeEqual on failed verify
- 4-8 digit PINs, generatePin() rejects weak patterns

Server actions (src/actions/water-log/{admin,field,settings}.ts)
- full Drizzle CRUD: headgates, irrigators, entries (with audit hooks)
- verifyPin + 8h field sessions, admin PIN + configurable 1-168h sessions
- regenerateAdminPin invalidates existing admin sessions
- threshold breach detection + alert log writes
- getWaterAdminSession() helper for admin pages

API routes
- /api/water-admin-auth: PIN-protected, generic error (no enum leak)
- /api/water-logs/export: admin-gated JSON/CSV, correct headers

UI (Field Almanac — cream + forest, Fraunces display, § section markers)
- src/components/admin/WaterLogAdminPanel.tsx: full rewrite
- src/components/water/WaterFieldClient.tsx: matches palette
- src/components/water/icons/{WaterGauge,SeasonMark}.tsx: custom SVGs
- src/app/admin/water-log/settings/page.tsx: full rewrite, Field Almanac

Tests
- tests/unit/water-log-pin.test.ts (21 cases): validate, hash, verify, generate
- tests/unit/water-log-reporting.test.ts (31 cases): season, filter, CSV, report
- tests/water-log.spec.ts (Playwright E2E): PIN form, auth gates, API gates

Docs
- docs/water-log.md: full admin guide, security model, data dictionary, checklist
- README: link to docs/water-log.md in admin modules list
- .env.example: Water Log section comment

Type-check: clean. Tests: 70 pass, 3 fail (3 pre-existing getAdminUser
failures on main, unrelated to this work).
2026-06-17 11:36:00 -06:00

322 lines
12 KiB
Markdown

# Route Commerce
A multi-tenant B2B e-commerce platform for fresh produce wholesale distribution. Brands manage products, stops, orders, and wholesale customers from a single admin dashboard.
## What It Does
Route Commerce helps produce brands run their wholesale operations:
- **Product catalogs** — Manage products with pricing, variants, and images
- **Stop/route scheduling** — Schedule pickup stops across regions with customer tracking
- **Order management** — Handle pickup and shipping orders, track fulfillment
- **Wholesale portal** — Self-service portal for wholesale customers to place orders
- **Billing** — Stripe-based subscription billing with plan tiers and add-ons
- **Communications** — Email/SMS campaigns via Harvest Reach module
- **Square sync** — Optional inventory sync with Square POS
## Tech Stack
- **Framework:** Next.js 16 (App Router)
- **Database:** Supabase (Postgres + Auth + RLS)
- **Payments:** Stripe + Square
- **Email/SMS:** Resend
- **Styling:** Tailwind CSS v4
## Getting Started
### 1. Clone and install
```bash
git clone <your-repo-url>
cd route-commerce-platform
npm install
```
### 2. Set up environment variables
Create a `.env.local` file with:
```bash
# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# Stripe
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_STARTER=price_...
STRIPE_PRICE_FARM=price_...
STRIPE_PRICE_ENTERPRISE=price_...
# Resend (email)
RESEND_API_KEY=re_...
```
### 3. Set up the database
Link your Supabase project, then push migrations:
```bash
supabase link --project-ref <your-project-ref>
npm run migrate
```
Or push a single migration:
```bash
npm run migrate:one 83
```
### 4. Start the dev server
```bash
npm run dev
```
Open [http://localhost:3000](http://localhost:3000). The dev server auto-runs `fix-agents.js` to patch Next.js App Router agent issues.
### 5. Dev auth bypass
In development, `/login` sets a `dev_session` cookie for full platform access:
- `dev_session=platform_admin` — full access, all brands
- `dev_session=brand_admin` — brand-scoped access only
Visit `/login` as a shortcut to set the platform_admin session.
## Key Commands
```bash
npm run dev # Start dev server
npm run build # Production build
npm run lint # ESLint
npm run migrate # Push all pending migrations
npm run migrate:one 83 # Push migration 083_*.sql only
npx tsc --noEmit # TypeScript check (no emit)
npx playwright test # Run E2E tests
```
## Project Structure
```
src/
├── actions/ # Server actions (database writes)
├── app/
│ ├── admin/ # Admin dashboard pages
│ ├── api/ # API routes (Stripe, Resend, Square webhooks)
│ └── storefront/ # Public brand storefront pages
├── components/ # Shared React components
│ ├── admin/ # Admin-specific components
│ └── storefront/ # Storefront components
└── lib/ # Utilities, auth, feature flags, formatting
supabase/
└── migrations/ # SQL migrations (numbered sequentially)
```
## Plan Tiers
| Tier | Price | Includes |
|---|---|---|
| **Starter** | $49/mo | Products, 10 stops/mo, Orders, Basic Pickup, 1 user, 25 products |
| **Farm** | $149/mo | Everything in Starter + Wholesale Portal, Harvest Reach, unlimited stops/products, 5 users |
| **Enterprise** | Custom | Everything in Farm + AI Intelligence Pack, SMS Campaigns, Square Sync, Water Log, unlimited users |
**Add-ons (à la carte on any plan):**
- Wholesale Portal ($99/mo)
- Harvest Reach email/SMS ($79/mo)
- AI Intelligence Pack ($59/mo)
- Water Log ($39/mo)
- Square Sync ($39/mo)
- SMS Campaigns ($29/mo)
## Time Tracking
### Adding Workers
1. Go to `/admin/settings` → expand **Workers & PINs**
2. Click **+ Add Worker** — enter name, role (Worker or Time Admin), language (EN/ES)
3. PINs are auto-generated. Click **Reset PIN** next to any worker to issue a new one
### Adding Tasks
1. Go to `/admin/settings` → expand **Tasks**
2. Click **+ Add Task** — enter EN name, optional ES name, unit (hours/pieces/units), sort order
3. Tasks appear in the time clock app for workers to clock into
### Notification Alerts
1. Go to `/admin/settings` → expand **General Settings**
2. Add email addresses and/or SMS numbers under **Notification Recipients**
3. Configure daily/weekly overtime alert thresholds
## Admin Settings Page
All brand configuration lives at `/admin/settings` with five sections:
- **General** — Pay period, overtime thresholds, alert settings, notification recipients
- **Workers & PINs** — Manage workers, reset PINs, toggle active/inactive
- **Tasks** — Define tasks workers can clock into
- **Users & Permissions** — Manage admin users and role flags
- **Integrations** — Stripe, Resend, Twilio, OpenAI credentials and sync options
## Admin Modules
The admin dashboard lives at `/admin`:
- **Command Center** — Overview dashboard
- **Orders** — All orders with pickup/ship status
- **Stops** — Route/stop scheduling and management
- **Products** — Product catalog management
- **Customers** — Wholesale customer management
- **Communications** — Harvest Reach campaign manager
- **Wholesale** — Wholesale portal settings
- **Billing** — Plan and subscription management
- **Water Log** — Irrigation tracking (add-on) — see [docs/water-log.md](docs/water-log.md)
- **Settings** — Brand settings, payments, apps
## Email Automations (Harvest Reach)
Automated email sequences for wholesale customer engagement.
### Abandoned Cart Recovery
3-email sequence (1hr → 24hr → 48hr) triggered when a wholesale customer adds items to cart but doesn't checkout.
**Cron schedule:** Every 6 hours via Vercel crons (`vercel.json`)
**Dashboard:** `/admin/communications/abandoned-carts`
**How it works:**
1. `detect_abandoned_wholesale_carts` RPC finds pending wholesale orders not yet enrolled
2. `enroll_abandoned_cart` RPC creates a tracking record with first email scheduled in 1 hour
3. Cron job calls `get_active_abandoned_carts` to find carts where `next_email_at <= now()`
4. `sendAbandonedCartEmail` fires the email via Resend, then updates `sequence_step`, `next_email_at`, and `status`
**Admin actions:**
- **View** — see all carts with status (Active/Recovered/Expired/Manually Closed), step, items, total
- **Close** — marks cart as manually_closed (stops further emails)
- **Resend** — resets `next_email_at` to now() and fires email immediately
### Welcome Email Sequence
4-email onboarding series triggered when a wholesale contact subscribes (email_opt_in = TRUE).
**Cron schedule:** Every 6 hours (same as abandoned cart)
**Dashboard:** `/admin/communications/welcome-sequence`
**How it works:**
1. Contact subscribes via wholesale portal → `enroll_welcome_sequence` RPC is called
2. Cron job calls `get_active_welcome_sequence` for entries where `next_email_at <= now()`
3. `sendWelcomeEmail` fires via Resend, updates step, schedules next email 24h later
4. After step 4, status → `completed`
**Admin actions:**
- **Resend** — resets entry to step 1 and fires immediately
- Unsubscribed/bounced contacts are skipped automatically
### Manual Trigger (curl)
```bash
# Abandoned cart — simulate cron
curl -X POST https://route-commerce-platform.vercel.app/api/email-automation/abandoned-cart \
-H "Authorization: Bearer $CRON_SECRET" \
-H "Content-Type: application/json"
# Welcome sequence
curl -X POST https://route-commerce-platform.vercel.app/api/email-automation/welcome-sequence \
-H "Authorization: Bearer $CRON_SECRET" \
-H "Content-Type: application/json"
# Check status / health
curl https://route-commerce-platform.vercel.app/api/email-automation/abandoned-cart
```
### Environment Variables Required
```bash
RESEND_API_KEY=re_... # Resend API key for sending emails
FROM_EMAIL="Tuxedo Corn <no-reply@routecommerce.com>" # Sender address
CRON_SECRET=... # Bearer token to secure cron endpoints (generate a long random string)
```
### Troubleshooting
| Problem | Likely cause | Fix |
|---|---|---|
| Emails not sending | `RESEND_API_KEY` not set | Check env var in Vercel dashboard |
| 401 Unauthorized on cron | `CRON_SECRET` mismatch | Verify local `.env.local` matches Vercel env |
| Empty abandoned cart table | No pending wholesale orders | Create test order, add items, abandon checkout |
| Welcome sequence not enrolling contacts | `enroll_welcome_sequence` not called | Ensure subscription UI calls the RPC on email_opt_in = TRUE |
### Testing Locally
```bash
# Run abandoned cart with verbose output
curl -X POST http://localhost:3000/api/email-automation/abandoned-cart \
-H "Authorization: Bearer local-dev-secret" \
-H "Content-Type: application/json"
# Response: { ok: true, results: { "Tuxedo Corn": { sent: 0, failed: 0, skipped: 0 }, ... } }
```
Set `CRON_SECRET=local-dev-secret` in `.env.local` for local testing.
## Go-Live Checklist
Use this checklist before adding real products and doing a soft launch.
### Pre-Launch
- [ ] Set `CRON_SECRET` in Vercel project environment variables (generate with `openssl rand -hex 32`)
- [ ] Verify `RESEND_API_KEY` is set in Vercel (for email sending)
- [ ] Verify `FROM_EMAIL` is set (e.g. `"Tuxedo Corn <no-reply@routecommerce.com>"`)
- [ ] Run `npm run build` locally — must pass clean with zero TypeScript errors
- [ ] Deploy latest commit to production: `vercel --prod`
- [ ] Smoke test: visit `/admin`, `/admin/settings`, `/tuxedo` — no console errors
### Adding Your First Brand's Products
1. Go to `/admin/products`**Add Product**
2. For each product: name, wholesale price, image, shipping type (pickup/ship/all)
3. Set `is_taxable` if Colorado sales tax applies
4. Make products **active** — inactive products don't show on storefront
5. For Tuxedo Corn: use brand slug `tuxedo`, set brand accent `green`
### Adding Pickup Stops
1. Go to `/admin/stops`**Create Stop**
2. Fill city, state, address, date, time, cutoff date
3. Assign brand — stops show on storefront at `/{brand-slug}#stops`
4. Set `is_public: true` to display on storefront
### Verifying Wholesale Portal
1. Go to `/admin/wholesale` — confirm portal is enabled
2. Visit `/{brand-slug}/wholesale/portal` as a customer
3. Add items to cart, start checkout — confirm abandoned cart appears in `/admin/communications/abandoned-carts` within minutes
### Verifying Email Automations
```bash
# Manually trigger welcome sequence (use a real email you control)
curl -X POST https://route-commerce-platform.vercel.app/api/email-automation/welcome-sequence \
-H "Authorization: Bearer $CRON_SECRET"
# Check Resend dashboard — should see sent emails
# Check /admin/communications/welcome-sequence — entry should show "Active"
```
### Monitoring Production
- Check Vercel dashboard → **Functions** logs for any 500 errors
- Check `/admin/communications/abandoned-carts` for recovered orders
- Vercel crons fire every 6 hours — check function invocations in dashboard
## Notes
- All database writes go through **server actions** (`src/actions/`), not the Supabase JS client directly
- Dev mode bypasses Supabase auth via `dev_session` cookie — never use this in production
- Brand-scoped data is enforced at the application layer via `p_brand_id` parameters in SECURITY DEFINER RPCs
- Display dates use `formatDate()` (MM/DD/YYYY) — never raw `toLocaleDateString()`
- Migration files are numbered sequentially — never reuse numbers