# Demo user (staging / local) Local bootstrap account for testing Descrybe v2 as a **standalone full-admin sandbox**. Demo is **not** an A1 company member — staff use Admin → Users → Switch to user to act for customers. **Isolation cheat sheet:** [safe-test-fixtures.md](safe-test-fixtures.md) **QA walkthrough:** [qa-local-demo.md](qa-local-demo.md) ## Smoke / tests — Platform Demo only **MUST** run smoke checks, E2E, Postman, and automated tests against **Platform Demo** (`demo@descrybe.local` / `dk_demo_local_descrybe_test_key_v1`). Keep **A1 Slovenija** clean: **read-only** (dump-faithful billing/catalog inspection only — no process, upload, mutate, or mock-llm writes under A1). Full rules: [safe-test-fixtures.md](safe-test-fixtures.md). ## Credentials | Field | Value | |-------|--------| | Email | `demo@descrybe.local` | | Password | `DemoPass123!` | | `must_set_password` | `false` | | `is_platform_admin` | `true` | | `is_active` | `true` | | Company | **Platform Demo** (isolated sandbox) | | Plan | **Platform Demo** (`is_custom=true`, ~1M credits, unlimited SKUs, all feature gates ON) | `seed-demo` also upserts alias `demo@descrybe.test` / `DemoPass123!` (`-also-email`). Prefer **`.local`**. ### Local logins (after demo isolation) | Role | Email | Password | Notes | |------|-------|----------|-------| | Demo admin | `demo@descrybe.local` | `DemoPass123!` | Platform admin; **Platform Demo** only; UserSwitcher **Demo admin** under Platform Demo | | Demo alias | `demo@descrybe.test` | `DemoPass123!` | Same sandbox | | Primary A1 | `a1-primary@descrybe.local` | `DemoPass123!` | Clerk `user_30AqqJ8uepxvPUzDSqy81U5w6Ll`; **A1 Slovenija** only; limited legacy nav | Do **not** merge demo into A1. Switchable list groups demo under **Platform Demo** and A1 under **A1 Slovenija**. ### System assistant guide personas Empty Free-plan companies for System assistant QA (password `DemoPass123!`): ```bash cd apps/api go run ./cmd/seed-guide-personas -postgres "$env:DATABASE_URL" ``` | Email | Suggested scenario | |-------|--------------------| | `guide-feed-url@descrybe.local` | Add feed from URL | | `guide-upload-csv@descrybe.local` | Upload CSV | | `guide-shopify@descrybe.local` | Connect Shopify | | `guide-woocommerce@descrybe.local` | Connect WooCommerce | | `guide-mapping@descrybe.local` | Map fields | | `guide-process@descrybe.local` | Start processing | | `guide-api@descrybe.local` | API keys + curl examples | | `guide-support@descrybe.local` | Support ticket | | `guide-attributes@descrybe.local` | List / create attributes | | Surface | URL | |---------|-----| | Web login | http://localhost:28472/login -> `/dashboard` | | Marketing | http://localhost:28472/ | | API docs (RapiDoc) | http://localhost:28472/docs | | OpenAPI | http://localhost:28471/api/v1/openapi.yaml (or same-origin via Vite proxy) | | API | http://localhost:28471 | **Browser smoke:** prefer `demo@descrybe.local` on **Platform Demo** (keep A1 read-only). Supply the password via `DESCRYBE_SMOKE_PASS` (see root `.env.example`) or `codehelper connections set-secret --name local-descrybe`. Recipe: `spa_hydrate` / login on `/login`. Do **not** use this account or API key in production. ## Demo API key Seeded for **Platform Demo** (sandbox company). Stored only as a SHA-256 hash (`auth.HashAPIKey`); plaintext is local-only. | Field | Value | |-------|--------| | Key | `dk_demo_local_descrybe_test_key_v1` | | Prefix | `dk_demo_lo` | | Name | Demo local API key | ```http Authorization: Bearer dk_demo_local_descrybe_test_key_v1 ``` or ```http X-API-Key: dk_demo_local_descrybe_test_key_v1 ``` ```bash curl -s -H "Authorization: Bearer dk_demo_local_descrybe_test_key_v1" \ http://localhost:8080/api/v1/products?limit=1 ``` ## Billing / plan (A1 Slovenija — dump-faithful) Company Settings and `/api/billing/credits` must show **migrated A1 numbers**, not a fake demo 1M / “Legacy” / “Local Demo Co” rename. | Field | MySQL source | Value | |-------|--------------|-------| | Company name | `companies.name` | `A1 Slovenija` | | PG company id | — | `604f23a8-b66e-4b21-8b45-0d72b68f4790` | | `legacy_company_id` | MySQL `companies.id` | `97e1a309-3d23-4aa2-b518-8e8d7afdfec7` | | Plan | `plans` + `company_plans.plan_id` | **A1** (`is_custom=true`, `is_legacy=false`, `term=yearly`, `yearly_credits=20000`, `monthly_credits=0`) — **pay-as-you-go** (credits wallet). Open-ended contract (`contract_end_date` null) is intentional for this demo tenant; `company_plans.notes` documents PAYG. | | Allocated | `company_plans.total_credits_allocated` | `2500` | | Wallet total | `credit_balances.total_credits` | `2500` | | Wallet used | `credit_balances.used_credits` | `163` | | Remaining | computed | `2337` | | Language / merge GTIN | `company_settings` | `sl` / `false` | | Usage snapshot | `usage_metrics` | folded into `company_settings.settings._legacy_usage` (dump-faithful) | | `billing_cycles` / `cost_metrics` in dump | — | **empty** for A1; PG cycle mirrors contract window with `credits_used=163` | `seed-demo` **preserves** this wallet for the A1 cohort (does not `AssignPlan` over it — that would zero `used_credits` and inflate totals). ### Fresh processing state (separate from seed) After `npm run seed:a1` / `go run ./cmd/seed-a1 -mode reimport …`, clear processing history without wiping catalog: ```powershell cd apps/api $env:DATABASE_URL = "postgres://descrybe:descrybe@localhost:5433/descrybe?sslmode=disable" go run ./cmd/seed-a1-reset-processing # optional: -dry-run | -name "A1 Slovenija" ``` Retains feeds, mappings, raw/mapped products, attributes, categories. Deletes processing jobs/job-products and processed_products; sets all raw to `unprocessed`. ### Local processing when credits run low Remaining **2337** is enough for light local smoke. For heavy AI runs without rewriting dump usage history: 1. Prefer platform-admin **AddCredits** (increases `total_credits` only; keeps `used_credits` / `_legacy_usage` accurate), or 2. Process under a separate non-A1 sandbox company. Do **not** re-seed a 1M “demo pack” onto A1 — that confuses Company Settings. ```sql SELECT c.name, p.name AS plan, p.monthly_credits, p.yearly_credits, p.term, p.is_custom, cp.total_credits_allocated, cb.total_credits, cb.used_credits, (cb.total_credits - cb.used_credits) AS remaining FROM companies c JOIN company_plans cp ON cp.company_id = c.id AND cp.is_active JOIN plans p ON p.id = cp.plan_id JOIN credit_balances cb ON cb.company_id = c.id WHERE c.legacy_company_id = '97e1a309-3d23-4aa2-b518-8e8d7afdfec7'; ``` ```powershell # A1 wallet check: login as a1-primary@ (or Admin switch), NOT demo@ — expect plan=A1 remaining≈2337 # demo@ lands on Platform Demo (custom plan / high credits), not A1 curl -s -b cookies.txt http://localhost:8080/api/billing/credits curl -s -b cookies.txt http://localhost:8080/api/me ``` ## How it was created ```powershell cd apps/api $env:DATABASE_URL = "postgres://descrybe:descrybe@localhost:5433/descrybe?sslmode=disable" go run ./cmd/seed-demo -postgres $env:DATABASE_URL # default email is demo@descrybe.local — override with -email if needed ``` `seed-demo` upserts the demo user(s) (argon2id), sets platform admin, ensures **Platform Demo** company (never renames A1), binds membership **only** to that company (removes stray A1 memberships), upserts the demo API key on Platform Demo, and assigns the custom **Platform Demo** plan (full features / high credits). A1 wallet and plan stay dump-faithful — see Billing section below. New self-serve signups still get **Free** — see [free-tier.md](free-tier.md). ### Company-admin promote (migrator) Imported cutover memberships default to `role=member`. Local Demo does **not** need `-promote-company-admins` — `seed-demo` already grants **admin** on Platform Demo. ```powershell cd apps/api # Inventory (read-only). Expect a1-primary@ on A1 with a1=true; demo@ will not appear (already admin). go run ./cmd/migrator -list-member-memberships -postgres $env:DATABASE_URL # Promote non-A1 only (A1 always skipped). Scope with -email / -user-id / -company-id. go run ./cmd/migrator -promote-company-admins -company-id -dry-run -postgres $env:DATABASE_URL go run ./cmd/migrator -promote-company-admins -company-id -confirm -postgres $env:DATABASE_URL ``` **Never** promote `a1=true`. Full ops notes: [cutover.md](cutover.md#promote-company-admins-rolemember--admin). Empty local register (no migrated catalog): ```powershell pwsh -File .\scripts\seed-local.ps1 ``` ## Primary company (Platform Demo) Login lands on **Platform Demo**. A1 Slovenija (`604f23a8-…` / legacy `97e1a309-…`) stays a separate customer tenant — switch via Admin impersonation, not demo membership. ### Completeness vs legacy MySQL (A1 Slovenija) Verified after migrator live load + dump-faithful billing restore. MySQL source: `descrybe_new` company `97e1a309-3d23-4aa2-b518-8e8d7afdfec7`. | Entity | MySQL (A1) | Postgres (A1 Slovenija) | Status | |--------|----------:|-------------------------:|--------| | Categories | 119 | 120 | OK (+1 local CSV smoke `Test Category`) | | Attributes | 303 | 303 | OK | | Category attributes | 307 | 307 | OK | | Custom variables | 3 | 3 | OK | | Input feeds | 12 | 12 | OK | | Feed mappings | 11 | 11 | OK (ComTrade has no `field_mappings` in MySQL either) | | Category title/description formulas | 119 / 119 | 119 / 119 | OK — list API now returns templates + `has_*_formula` flags | | Standard fields (A1 dump keys) | 22 | 22 (+ ecommerce seed) | After ensure: ~44 enabled (dump keys kept + full ecommerce catalog for mapping) | | Export feeds | 2 | 2 | OK | | Processed products | 4321 | 4323 | OK (+2 local smoke samples) | | Raw products | 31062 | 23744 | OK intentional — **7320** GTIN deduped under `(company_id, gtin)` unique; +2 smoke | | Orphan FKs (company-scoped) | — | 0 | OK (mappings/company, processed→raw/feed, raw→feed, export→source) | Global migrator validation: `artifacts/validation-report.json` — `mode: live`, orphans **16/16 pass**. Attribute collapse globally (95k → 57k) is distinct-key unique constraint, not an A1 gap. | Company | ID | Input feeds | Products (processed) | Raw products | Export feeds | Categories | Mappings | |---------|-----|------------:|---------------------:|-------------:|-------------:|-----------:|---------:| | **A1 Slovenija** (default) | `604f23a8-b66e-4b21-8b45-0d72b68f4790` | 12 | ~4323 | ~23744 | 2 | 120 | 11 | | Descrybe | `3802e3c1-b292-4d1b-9fcf-eca193f364c8` | 10 | 2742 | 85608 | 2 | 2334 | 8 | | Merkur d.o.o. | `57921d6c-c65a-4867-b9df-e5cbe39d741b` | 3 | 151 | 9429 | 2 | 5442 | 2 | Counts drift slightly after CSV smoke uploads; always key off company id `604f23a8-…` / `legacy_company_id` `97e1a309-…`. ## App paths (accurate) | Task | Path | |------|------| | Products | `/products` (use **Unprocessed** if Processed looks empty — UI also auto-falls back) | | Feeds / mapping | `/feeds` → feed → mappings (Elkotex: `/feeds/7495b404-68d3-4f94-9568-1d2c7a81ee8a/mapping`) | | Category formulas | Categories → Edit → Title/Description formula (templates migrated; list shows `has_*_formula`) | | Standard fields | `/standard-fields` — Enable recommended if mapping targets look sparse after a fresh import | | CSV uploads | Dashboard Upload CSV; `/products`, `/categories`, `/attributes`; history at `/files` | | Process history | `/processing` (migrated A1 jobs when domain `jobs` was imported; also live jobs) | | Switch to Primary A1 | Login `a1-primary@descrybe.local` / `DemoPass123!`, or Admin → Users → **Switch to user** | | Brand + logo | `/brand` (`POST /api/brand/logo`) | | Campaigns | `/campaigns` | | Content calendar | `/marketing/calendar` | | SEO | `/seo` | | Reviews | `/woocommerce?tab=reviews` (nav `/reviews` redirects here) | | Email sending | `/integrations/email` | | Billing / plans | `/billing`, `/plans` | | Public pricing | `/pricing` | API CSV: `POST /api/products/upload`, `/api/categories/upload`, `/api/attributes/upload` (and `/import` aliases). Product search: `q` or `search`; `feed_id` filters supported. After backend route changes, **rebuild/restart the Go API** or smoke can show stale 404s. ## Switching company `POST /api/auth/select-company` with CSRF + session cookie: ```json { "company_id": "3802e3c1-b292-4d1b-9fcf-eca193f364c8" } ``` ## Smoke checks ```sql SELECT email, must_set_password, is_platform_admin, is_active FROM users WHERE email = 'demo@descrybe.local'; SELECT c.id, c.name, (SELECT COUNT(*) FROM processed_products p WHERE p.company_id = c.id) AS products, (SELECT COUNT(*) FROM input_feeds f WHERE f.company_id = c.id) AS feeds FROM companies c WHERE c.name = 'Platform Demo' ORDER BY c.created_at DESC LIMIT 1; -- expect Platform Demo sandbox (not A1 Slovenija / 604f23a8-…) ``` ```powershell cd apps/api $env:DATABASE_URL = "postgres://descrybe:descrybe@localhost:5433/descrybe?sslmode=disable" go run ./cmd/seed-demo -postgres $env:DATABASE_URL # Session login (CSRF double-submit cookie from any GET) $session = New-Object Microsoft.PowerShell.Commands.WebRequestSession try { Invoke-WebRequest http://127.0.0.1:8080/api/auth/me -WebSession $session -UseBasicParsing | Out-Null } catch {} $csrf = ($session.Cookies.GetCookies("http://127.0.0.1:8080") | Where-Object Name -eq descrybe_csrf).Value $body = '{"email":"demo@descrybe.local","password":"DemoPass123!"}' Invoke-WebRequest http://127.0.0.1:8080/api/auth/login -Method POST -WebSession $session ` -Headers @{ "X-CSRF-Token" = $csrf; "Content-Type" = "application/json" } -Body $body -UseBasicParsing Invoke-RestMethod http://127.0.0.1:8080/api/products?limit=3 -WebSession $session Invoke-RestMethod http://127.0.0.1:8080/api/feeds -WebSession $session Invoke-RestMethod http://127.0.0.1:8080/api/categories?limit=5 -WebSession $session ``` Expect: login **200**, default company **Platform Demo**, demo API key scoped to that company. A1 catalog counts (~4323 products) belong to **A1 Slovenija** only — switch as `a1-primary@descrybe.local`, do not process under demo. ```powershell cd apps/api; go build ./... cd ../web; npm run check ``` ## Related - [e2e-feeds-process-export.md](e2e-feeds-process-export.md) - [qa-local-demo.md](qa-local-demo.md) — login steps, nav status, uploads, known limits - [migration-run-log.md](migration-run-log.md) — full MySQL→PG migrator report - [marketing-suite-user-guide.md](marketing-suite-user-guide.md) - [free-tier.md](free-tier.md) - [go-live-checklist.md](go-live-checklist.md) - Repo root `README.md` ## Related - [api-surface-smoke.md](api-surface-smoke.md) — admin / healthz / docs / public v1 smoke - [qa-local-demo.md](qa-local-demo.md)