From 2db6c0149bbb54cf5522d537dadacae1a638957c Mon Sep 17 00:00:00 2001 From: openclaw Date: Tue, 9 Jun 2026 13:38:34 -0600 Subject: [PATCH] docs: add DB schema reference and Tuxedo seed script --- DB_REFERENCE.md | 125 +++++++++++++++++++++++++++++++++++++++++ scripts/seed-tuxedo.ts | 41 ++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 DB_REFERENCE.md create mode 100644 scripts/seed-tuxedo.ts diff --git a/DB_REFERENCE.md b/DB_REFERENCE.md new file mode 100644 index 0000000..4837327 --- /dev/null +++ b/DB_REFERENCE.md @@ -0,0 +1,125 @@ +# Database Reference — Neon Postgres (Route Commerce) + +## Core Auth +| Table | Key Columns | +|---|---| +| `users` | id, name, email, email_verified, image, created_at | +| `sessions` | id, user_id, token, expires_at, ip_address, user_agent | +| `accounts` | id, user_id, provider_id, access_token, refresh_token | +| `verifications` | id, identifier, value, expires_at | +| `admin_users` | id, user_id, email, name, **role**, can_manage_* (15 flags), created_at | +| `admin_user_brands` | admin_user_id, brand_id, added_at, added_by | + +> `role` values: `platform_admin`, `brand_admin`, `store_employee` +> Link a user to a brand via `admin_user_brands`, NOT a column on `admin_users`. + +## Brands & Plans +| Table | Key Columns | +|---|---| +| `brands` | id, name, slug, **plan_tier**, max_users, max_products, max_stops_monthly, stripe_* | +| `plans` | id, code, name, monthly_price_cents, max_users, max_products, max_stops_monthly, features | +| `add_ons` | id, code, name, monthly_price_cents, description | +| `brand_add_ons` | brand_id, add_on_id, stripe_subscription_id, status | +| `brand_features` | id, brand_id, feature_key, enabled, enabled_at | +| `brand_settings` | id, brand_id, legal_business_name, phone, email, logo_url, tagline, from_email, primary_color, custom_footer_text, etc. | +| `wholesale_settings` | id, brand_id, require_approval, min_order_amount, pickup_location, fob_location, from_email, invoice_business_name, last_invoice_number | + +## Products & Media +| Table | Key Columns | +|---|---| +| `products` | id, brand_id, name, description, sku, type, price_cents, inventory, unit, active, is_taxable, pickup_type, image_url | +| `product_images` | id, product_id, storage_key, position, alt_text | +| `wholesale_products` | id, brand_id, rc_product_id, name, unit_type, availability, qty_available, price_tiers (JSON) | + +## Orders +| Table | Key Columns | +|---|---| +| `orders` | id, brand_id, customer_id, stop_id, total_cents, status, fulfillment, customer_address, placed_at | +| `order_items` | id, order_id, product_id, quantity, price_cents, fulfillment | +| `stops` | id, brand_id, name, location, address, city, state, zip, date, time, cutoff_date, status, is_public | +| `shipments` | id, order_id, carrier, tracking_number, label_url, status, fedex_shipment_id | +| `shipping_settings` | id, brand_id, carrier, fedex_account_number, fedex_api_key, fedex_use_production | +| `user_carts` | id, brand_id, customer_id, items (JSON) | +| `abandoned_cart_recovery` | id, brand_id, customer_id, contact_email, sequence_step, status, next_email_at | + +## Wholesale +| Table | Key Columns | +|---|---| +| `wholesale_customers` | id, user_id, brand_id, company_name, contact_name, email, account_status, credit_limit, deposits_enabled | +| `wholesale_orders` | id, brand_id, customer_id, status, fulfillment_status, payment_status, subtotal, deposit_paid, balance_due, invoice_number | +| `wholesale_order_items` | id, wholesale_order_id, product_id, quantity, unit_price, line_total | +| `wholesale_deposits` | id, wholesale_order_id, amount, payment_method, reference | +| `wholesale_customer_product_pricing` | id, customer_id, product_id, custom_price_cents | +| `wholesale_notifications` | id, brand_id, customer_id, notification_type, channel, status, sent_at | +| `wholesale_webhook_settings` | id, brand_id, url, secret, enabled | +| `wholesale_sync_log` | id, brand_id, event_type, order_id, status | + +## Communications (Harvest Reach) +| Table | Key Columns | +|---|---| +| `communication_campaigns` | id, brand_id, name, subject, body_text, body_html, campaign_type, status, audience_rules (JSON), scheduled_at, sent_at | +| `communication_contacts` | id, brand_id, email, phone, first_name, last_name, email_opt_in, sms_opt_in, unsubscribed_at, tags | +| `communication_message_logs` | id, brand_id, campaign_id, contact_id, customer_email, delivery_method, subject, status | +| `communication_segments` | id, brand_id, name, rules (JSON) | +| `communication_templates` | id, brand_id, name, subject, body_text, body_html, template_type | +| `communication_settings` | id, brand_id, default_sender_email, email_provider | +| `welcome_email_sequence` | id, brand_id, contact_id, sequence_step, status, next_email_at | + +## Customers +| Table | Key Columns | +|---|---| +| `customers` | id, brand_id, primary_email, primary_phone, first_name, last_name, source | +| `customer_communication_preferences` | id, customer_id, email_opt_in, sms_opt_in | + +## Time Tracking +| Table | Key Columns | +|---|---| +| `time_tracking_workers` | id, brand_id, name, role, pin, active | +| `time_tracking_tasks` | id, brand_id, name, unit, sort_order, active | +| `time_tracking_logs` | id, brand_id, worker_id, task_id, clock_in, clock_out, lunch_break_minutes | +| `time_tracking_settings` | id, brand_id, pay_period_start_day, daily_overtime_threshold, weekly_overtime_threshold, overtime_multiplier | +| `time_tracking_notification_log` | id, brand_id, worker_id, notification_type, status, sent_at | + +## Water Log +| Table | Key Columns | +|---|---| +| `water_headgates` | id, brand_id, name, active | +| `water_irrigators` | id, brand_id, name, pin_hash, active | +| `water_sessions` | id, irrigator_id, expires_at | +| `water_log_entries` | id, brand_id, headgate_id, irrigator_id, measurement, unit, notes, logged_at | +| `water_alert_log` | id, brand_id, alert_type, headgate_id, message, sent_to | + +## Integrations +| Table | Key Columns | +|---|---| +| `square_sync_log` | id, brand_id, sync_type, direction, item_id, status | +| `square_sync_queue` | id, brand_id, sync_type, status, retry_count | +| `api_keys` | id, brand_id, name, key_hash, permissions (JSON), is_active | +| `payment_settings` | id, brand_id, provider, stripe_publishable_key, stripe_secret_key, square_access_token | + +## Locations +| Table | Key Columns | +|---|---| +| `locations` | id, brand_id, name, address, city, state, zip, active | + +## Misc +| Table | Key Columns | +|---|---| +| `audit_logs` | id, user_id, brand_id, action, entity_type, entity_id, details (JSON) | +| `admin_action_logs` | id, admin_user_id, brand_id, action, target_type, target_id, metadata (JSON) | +| `operational_events` | id, brand_id, event_type, entity_type, actor_type, payload (JSON) | +| `referral_codes` | id, brand_id, referral_code, referrer_email, reward_type, reward_value | +| `referral_redemptions` | id, referral_code_id, brand_id, referred_user_id | +| `onboarding_progress` | id, brand_id, user_id, current_step, completed_steps (JSON) | +| `notification_preferences` | id, user_id, email_orders, email_marketing, sms_orders, etc. | +| `changelogs` | — | +| `changelog_reads` | id, user_id, changelog_id | +| `_migrations` | filename, applied_at | + +## Schema Notes +- All timestamps use `TIMESTAMP WITH TIME ZONE` — timezone-aware +- No PostgreSQL ENUM types — statuses are plain TEXT +- Brand scoping enforced at application layer (SECURITY DEFINER RPCs), NOT via RLS +- `brands.slug` is the public storefront URL slug (e.g. `tuxedo`, `indian-river-direct`) +- `admin_users.user_id` links to `neon_auth.user` (managed by Neon Auth), NOT to `users` table +- The `users` table above is Neon Auth's internal table — do NOT write to it directly diff --git a/scripts/seed-tuxedo.ts b/scripts/seed-tuxedo.ts new file mode 100644 index 0000000..ff6ef9b --- /dev/null +++ b/scripts/seed-tuxedo.ts @@ -0,0 +1,41 @@ +import { config } from "dotenv"; +config({ path: ".env.local" }); +import pg from "pg"; + +const { Pool } = pg; + +const TUXEDO = "64294306-5f42-463d-a5e8-2ad6c81a96de"; + +async function main() { + const pool = new Pool({ connectionString: process.env.DATABASE_URL }); + + try { + // brand_settings + const bs = await pool.query( + `INSERT INTO brand_settings (brand_id, legal_business_name, phone, email, logo_url, tagline, from_email) + VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (brand_id) DO UPDATE SET legal_business_name = EXCLUDED.legal_business_name + RETURNING brand_id, legal_business_name`, + [TUXEDO, "Tuxedo Corn LLC", "970-555-0100", "info@tuxedocorn.com", + "/brand-logos/64294306-5f42-463d-a5e8-2ad6c81a96de/logo.png", + "Farm-Fresh Sweet Corn, Delivered", "orders@tuxedocorn.com"] + ); + console.log("brand_settings:", bs.rows[0]); + + // wholesale_settings + const ws = await pool.query( + `INSERT INTO wholesale_settings (brand_id, require_approval, pickup_location, fob_location, from_email) + VALUES ($1, true, $2, $3, $4) + ON CONFLICT (brand_id) DO UPDATE SET pickup_location = EXCLUDED.pickup_location + RETURNING brand_id, pickup_location`, + [TUXEDO, "59751 David Road, Olathe, CO 81425", "FOB Olathe, CO", "orders@tuxedocorn.com"] + ); + console.log("wholesale_settings:", ws.rows[0]); + + console.log("\nDone!"); + } finally { + await pool.end(); + } +} + +main().catch((e) => { console.error(e.message); process.exit(1); });