# VetOne — Cloud Demo Provisioning: Full Specification

> Status: **DRAFT for approval** · Owner: TPS · Branch: `feat/productcompat-ai` (spec only; build on a new `feat/vetone-demo` branch)
> Golden rule for this whole effort: **module-only, core stays byte-identical.** No edits under `app/`, `resources/`, `routes/` core; everything ships inside modules using existing seams (`get_additional_script`, module service providers, `BusinessUtil`, seeders).

---

## 1. Goal

Publish **VetOne** (the veterinary vertical) as a self-service cloud demo. A prospect fills a registration page, and the system automatically:

1. Captures business + owner + staffing details and a logo.
2. Generates a unique **slug** (subdomain-ready) and a **branded pet-owner PWA** + **branded staff PWA**.
3. Provisions a full working vet tenant: modules entitled, roles seeded, **demo users** (admin, 3 doctors with different slots, nurses, reception, accounting, HR…), and demo clinical data (SOAP options, medicines, AI prompts, vaccines, appointments).
4. Runs a pre-login **onboarding gate**: connect WhatsApp (QR) and enter a Groq API key — both **skippable**.
5. Ships a **very detailed, replayable, media-rich guided tour**.
6. Gives the **TPS team** a heat-map + incident ("mistakes") report per user and per business to evaluate each trial.
7. Expires each demo after **48 hours** → soft-locks and redirects to a "contact TPS" landing.

Deliverable to cloud = **a zip of the code + a seeded DB (with seeders)** and a deploy runbook.

---

## 2. Confirmed decisions

| # | Decision | Choice |
|---|---|---|
| D1 | Tenant isolation | **Single-DB multi-tenant** — one new `business` row inside the existing `vet_one_demo` project DB (NOT a DB per tenant). |
| D2 | Addressing | **Path-slug now** (`vetone.app/t/{slug}`), resolver **designed for wildcard subdomains later** with no rewrite. |
| D3 | 48h expiry | **Soft-lock + redirect to "contact TPS"**; keep data as a lead; `demo:purge` cron deletes after retention window (default 14 days). |
| D4 | Staff PWA branding | **Runtime-brand pwa-staff too** (add brand store + staff brand endpoint, like pwa-owner). Rebuild pwa-staff once. |
| D5 | Demo AI when Groq skipped | **AI off** (graceful "not configured") until the tenant adds their own key. No pooled/shared key. |
| D6 | Extra modules | Include **AdvancedTransfer, PurchaseRequisition, ActivityLog** (already enabled ride-along modules — assign their permissions to demo roles). |
| D7 | Tracking/heatmap home | **Enhance the existing `ActivityLog` module** (do not create a separate module). |
| D8 | Tour media hosting | In-app `storage/` only, served through a **protected, non-downloadable** stream (hardened against IDM/download managers). |

---

## 3. Architecture overview

- **Tenancy:** existing `business_id` single-DB pattern. The vet vertical lives in the `vet` SaaS project (`proj_vet` → DB `vet_one_demo`) per `Modules/SaasSwitcher/Config/config.php`.
- **Entitlements:** per-business `business.module_entitlements` JSON + `entitlements:seed --project=vet`. Vet set: `VetClinic, VetAdmission, VetAI, VetPortal, VetSite, VetStaffPortal, NX600Lab, IdexxLab, AbaxisLab`.
- **Slug/brand:** existing `portal_brands` table (`slug`, `name`, `logo_url`, `accent_hex`, contacts, `login_methods`) resolved at runtime by `Modules/PortalApi/Http/Controllers/Api/PortalController@brand`. **PortalApi is currently disabled and MUST be enabled.**
- **PWAs:** pwa-owner already runtime-branded via `/api/v1/portal/brand/{slug}`; pwa-staff to be upgraded to the same pattern (D4). Build outputs already served from `public/app` and `public/staff-app`.
- **AI:** per-tenant encrypted keys in `ai_providers` (AiCore). Groq preset already in `Modules/AiCore/Config/config.php`. Graceful failover means skipped keys just disable AI (D5).
- **WhatsApp:** live QR pairing already exists at `/whatsapp/channels/{id}/connect` (driver=web), per-tenant `wa_channels`.
- **Audit/telemetry:** `enhanced_activity_logs` already captures `user_id, business_id, action, model_type, model_label, before/after_data, changed_fields, request_url, request_method, ip_address, user_agent, request_id, is_primary, created_at`.

---

## 4. Module inventory for VetOne

### 4.1 Core vet product (enable + entitle)
| Module | Ver | Role |
|---|---|---|
| VetClinic | 2.2.0 | Hub — appointments, pets/owners, prescriptions, vaccinations, exam/SOAP templates, **doctor schedules/slots** (`vc_doctor_schedules`) |
| VetAdmission | 2.0.0 | Reception / admission (requires VetClinic) |
| VetAI | 1.1.0 | AI assistant + voice transcription (requires AiCore) |
| VetPortal | 1.0.0 | Pet-owner portal login → backs **pwa-owner** |
| VetStaffPortal | 1.0.0 | Staff bearer API → backs **pwa-staff** |
| VetSite | 1.0.0 | Public clinic website (subdomain-aware) |

### 4.2 Shared platform (already enabled, ride-along, never gated)
Essentials 5.2 · AiCore 1.4.2 · WhatsApp · SaasSwitcher (entitlements — mandatory) · DemoLogin · **ActivityLog 1.2.0** · Accounting · **AdvancedTransfer** · **PurchaseRequisition**
→ Only action needed: assign their permissions to the seeded demo roles.

### 4.3 Must-enable
- **PortalApi** — currently `false` in `modules_statuses.json`; without it the branded owner PWA shell + brand API do not load. Set `true`, run its install/migrate.

### 4.4 Entitled but device-link OFF in the demo
`LabCore`, `NX600Lab`, `IdexxLab`, `AbaxisLab` — modules load and the lab order/result UI is visible, but **no LabBridge agent** is connected so they show "Agent: Offline" and never sync real analyzer data. This is the "lab integration disabled in demo" behaviour.

### 4.5 Held back (until tested + approved)
`NeoSkin`, `TurboBoost`.

---

## 5. New / changed components

| Component | Kind | Summary |
|---|---|---|
| **VetOneDemo** | new module | registration wizard, provisioner, slug resolver, onboarding gate, 48h expiry middleware, `demo:purge`, expired landing |
| **GuidedTour** | new module | Shepherd.js replayable media-rich tour, DB-authored steps, protected media stream, authoring UI, "Play tour ▶" launcher |
| **ActivityLog** | enhance (1.2.0 → 1.3.0) | incident/"mistakes" capture + heat-map + per-tenant trial evaluation report, host-only (TPS) views |
| **pwa-staff** | change | add runtime brand store + staff brand endpoint (D4) |
| **PortalApi** | enable | owner PWA shell + brand API |

---

## 6. Feature specifications

### F1 — Registration wizard (`VetOneDemo`)
**Routes** (module `web.php`, guest):
- `GET /vetone/register` — form
- `POST /vetone/register` — submit → provision → redirect to onboarding gate
- `POST /vetone/register/check-slug` — async slug uniqueness

**Fields captured**
- Business: **name**, **logo** (upload), **address** (country/state/city/zip/landmark), **contact numbers** (primary phone, WhatsApp number), optional accent colour (auto-derived from logo if omitted).
- Owner: **owner name** (first/last), **email**, phone.
- Staffing counts: **# doctors**, **# nurses**, **# receptionists**, **# accounting**, **# HR**, (optional # warehouse, # assistants).

**Validation:** email unique per project DB; slug unique in `portal_brands`; logo image ≤ 2 MB (png/jpg/webp/svg); counts 0–20 each; at least 1 doctor.

**Slug generation:** `Str::slug(name)`, collision-suffixed (`-2`, `-3`…). Stored on `portal_brands.slug` and mirrored to a new `business.demo_slug` for reverse lookup.

### F2 — Provisioner service (`VetOneDemo\Services\DemoProvisioner`)
Runs in a single DB transaction on the `proj_vet` connection:

1. **Business + owner** via existing `App\Utils\BusinessUtil::createNewBusiness()`, `addLocation()`, `newBusinessDefaultResources()` (reuse; no core edits).
2. **Entitlements:** set `business.module_entitlements` = vet set (§3); set `demo_created_at = now()`, `demo_expires_at = now()->addHours(48)`, `demo_slug`.
3. **Brand row** in `portal_brands`: slug, name, logo_url, accent_hex, contact_phone, contact_email, address, `login_methods`. → owner + staff PWAs branded instantly (no rebuild).
4. **Roles:** run `Modules\VetClinic\Services\DefaultRolesSeeder` for the business (Receptionist, Doctor, Nurse, Assistant, Branch Manager, Accounting), then attach AdvancedTransfer / PurchaseRequisition / Accounting / ActivityLog permissions to the appropriate roles (D6).
5. **Demo users** (passwordless-capable per DemoLogin pattern, plus a shown starter password):
   - 1 **Admin**.
   - **N Doctors** — each gets a **distinct `vc_doctor_schedules`** (different weekday/slot windows and colours) so the appointment calendar looks realistic. Default N=3.
   - **N Nurses, Receptionists, Accounting, HR** per entered counts.
6. **Clinical/demo seeders** (existing — wire into provisioning):
   - `SoapOptionsSeeder` (**SOAP**), `MedicineCategoriesSeeder` + `DefaultMedicinesSeeder` (**medicine**), `VaccineTypesSeeder`, `AppointmentTypesSeeder`, `AppointmentReasonsSeeder`, `ConsentTemplatesSeeder`, `MedicalConditionsSeeder`.
   - **VetAI prompts:** seed the vet-vertical AI prompts via AiCore (`AiPrompts` / `VerticalProfile` defaults) into the tenant so VetAI has working prompts out of the box (D-note: no provider key yet → prompts present but inactive until Groq added, per D5).
   - Sample pets/owners + a handful of appointments spread across the 3 doctors + one admission, for a populated demo.
7. On success → auto-login owner and route to **onboarding gate** (F4).

### F3 — Slug resolver (subdomain-ready)
`VetOneDemo\Support\TenantResolver`:
- **Now:** reads slug from path `/t/{slug}` and from `portal_brands.slug`.
- **Later:** same resolver accepts `Host` header (`{slug}.vetone.app`); enabling subdomains = DNS wildcard + wildcard TLS + a config flag — no code rewrite. PWAs continue keying off `portal_brands.slug`.

### F4 — Onboarding gate (pre-login, both skippable)
Modeled on `Modules\MedRegistry\Http\Controllers\SetupWizardController`.
- **Step A — WhatsApp:** "Connect WhatsApp" creates a `wa_channels` row (driver=web) and shows the existing live QR poll (`/whatsapp/channels/{id}/connect`). **Skip** button.
- **Step B — Groq:** paste API key → saved to per-tenant encrypted `ai_providers` (`svc_groq`) via `AiSettingsController::saveService`; includes an embedded **"How to get a Groq API key"** doc (console.groq.com → API Keys → Create key). **Skip** button.
- **D5 behaviour:** if skipped, VetAI + WA auto-reply show **"not configured"** and stay inactive until a key is added later at `/ai-core/settings`.
- **Step C — Done:** land in the dashboard with the guided tour auto-starting once (F7).

### F5 — 48-hour demo lifecycle (`VetOneDemo`)
- **Schema:** new nullable columns on `business`: `demo_created_at`, `demo_expires_at`, `demo_slug`, `demo_status` (`active|expired|purged`). Added via a VetOneDemo migration (same pattern SaasSwitcher used for `module_entitlements`).
- **Middleware `EnsureDemoActive`** on all tenant routes: if `demo_expires_at < now()` → set `demo_status=expired`, log the user out, redirect to `GET /demo-expired`.
- **Expired landing** (`/demo-expired`): branded with the tenant's own logo/name; message "Your 48-hour VetOne demo has ended", with **TPS contact block** (see §10).
- **Cron `demo:purge`:** deletes tenants where `demo_expires_at + retention(14d) < now()` — business, users, `portal_brands`, `ai_providers`, `wa_channels`, tour progress, and demo data; sets `demo_status=purged` on a lightweight audit stub kept for reporting.

### F6 — Staff PWA runtime branding (`pwa-staff`)
- Backend: staff brand endpoint returning `PortalBrand::toPwaPayload()` behind the staff bearer resolver.
- Frontend: add `src/stores/brand.ts` + `brand.load()` in `main.ts` (mirror pwa-owner); replace hardcoded indigo tokens in `src/assets/main.css` with runtime `--c-brand-*`. Rebuild once → `public/staff-app/`.

### F7 — Guided tour (`GuidedTour` module)
**Engine:** Shepherd.js (rich HTML per step → embed image/video, spotlight, progress, back/next).

**Data model (DB-authored, not hardcoded):**
- `tour_flows` (id, key, title, vertical, is_active, autostart_once)
- `tour_steps` (id, flow_id, order, route, target_selector, title, body_html, media_id nullable, placement)
- `tour_media` (id, kind `image|video`, storage_path, mime, original_name, checksum)
- `tour_progress` (id, user_id, flow_id, last_step, completed_at)

**Behaviour:**
- **"Play tour ▶" launcher** injected globally through the existing `get_additional_script` asset seam (no core edits). Available on every page; replayable anytime.
- **Autostart once** for a new demo user; state in `tour_progress`.
- **Multi-page:** a step carries a `route`; the tour navigates there and resumes via a short-lived resume token, so one tour walks appointments → SOAP → prescriptions → owner PWA → settings end-to-end.
- **Authoring UI** (admin/TPS): create/reorder steps, attach an image or video, set target selector + page. No code needed to extend the tour.

**Media protection (D8) — served from `storage/`, hardened against download / IDM:**
- Media is **not** in `public/`; it is streamed by an authenticated controller `GET /tour/media/{signedId}` (Laravel signed URL, short TTL, per-session token).
- Response headers: `Content-Disposition: inline`, `Cache-Control: no-store, private`, `X-Content-Type-Options: nosniff`, and **`Accept-Ranges: none` with `Range` requests rejected (416)** — this specifically defeats IDM/segmented download managers that rely on range requests.
- Same-origin **Referer/Origin check**; requests without the app origin are 403.
- Video element hardening: `controlsList="nodownload noremoteplayback"`, `disablePictureInPicture`, context-menu disabled, played from a **blob/MediaSource** URL (no stable direct link).
- **Honest limitation (documented for TPS):** these measures stop casual saving and download managers; they cannot prevent screen recording. No client-side DRM is claimed.

### F8 — ActivityLog enhancement (tracking, heat-map, trial report) — host-only (TPS)
Enhance the existing module (bump `module_version` 1.2.0 → 1.3.0).

**8a. "Mistakes"/incident capture (module-only, no core edits):**
- New table `activity_incidents` (user_id, business_id, type `http_error|validation|denied|exception`, status_code, route, method, message, exception_class, file, line, url, ip, user_agent, request_id, created_at).
- **Terminating middleware `CaptureIncidents`** (registered by the module provider, pushed to the global stack) records 4xx/5xx responses, **403 permission-denials**, and **422 validation failures**.
- **`Log::listen`** in the module provider captures `error`/`critical` log entries (exceptions) with class/file/line — no edit to `App\Exceptions\Handler`.

**8b. Heat-map:**
- Aggregate `enhanced_activity_logs` by **user** and by **business** into a day×hour grid + module/action breakdown (indexes already exist on `business_id+model_type+action` and `created_at`).
- Views under `Modules/ActivityLog/Resources/views/insights/` with two tabs: **User heat-map**, **Business heat-map**.

**8c. Per-tenant trial evaluation report:**
- Per demo tenant: session count, **feature/module coverage** (distinct `model_type` touched), time-to-first-appointment, drop-off (last activity vs expiry), **incident count by type** (from 8a), and **WA/Groq configured-or-skipped** flags.
- One-screen "trial scorecard" so TPS can judge each 48h demo and follow up.

**8d. Access control:** all §8b/§8c screens are **host-only** — visible only on the host project (`static`, `is_host=true`) to a TPS super-admin permission (`activitylog.tps_insights`). Tenants never see cross-tenant data.

---

## 7. Data model summary (new)

| Table / column | Where | Purpose |
|---|---|---|
| `business.demo_created_at / demo_expires_at / demo_slug / demo_status` | VetOneDemo migration | 48h lifecycle + reverse slug lookup |
| `portal_brands` row (existing table) | provisioning | per-tenant PWA branding |
| `vc_doctor_schedules` rows (existing) | provisioning | 3 doctors, distinct slots |
| `tour_flows / tour_steps / tour_media / tour_progress` | GuidedTour | authored tour + progress |
| `activity_incidents` | ActivityLog | "mistakes" capture |

---

## 8. Routes summary (new)

```
GET/POST  /vetone/register                 VetOneDemo (guest)
POST      /vetone/register/check-slug       VetOneDemo (guest)
GET       /vetone/onboarding/{step}         VetOneDemo (auth, gate)
POST      /vetone/onboarding/wa|groq|skip   VetOneDemo (auth, gate)
GET       /demo-expired                     VetOneDemo (guest)
GET       /t/{slug}/...                      TenantResolver (subdomain-ready)
GET       /tour/media/{signedId}            GuidedTour (auth, signed, no-range)
GET/POST  /tour/admin/...                    GuidedTour (admin authoring)
GET       /activity-log/insights/*           ActivityLog (host-only, tps_insights)
```

---

## 9. Packaging & deployment (the zip)

**Code zip** (`git archive` of the release branch):
- Include pre-built `public/app` + `public/staff-app` (so no Node build on server).
- `modules_statuses.json` with `PortalApi: true`; exclude `node_modules`, `.env`, `bootstrap/cache/*`, `storage/*` runtime.

**DB zip:**
- `mysqldump` of a freshly seeded `vet_one_demo` (permissions, currencies, vet roles, one template business + sample data), **plus** the seeder classes so a clean host can also `php artisan migrate --seed` + `entitlements:seed --project=vet`.

**Runbook:**
1. Import SQL (or migrate+seed). 2. `.env`: DB, `APP_URL`, `DEMO_LOGIN=true`. 3. `php artisan module:enable PortalApi`. 4. `php artisan entitlements:seed --project=vet`. 5. `php artisan config:cache route:cache`. 6. Cron: `* * * * * php artisan schedule:run` (drives `demo:purge`). 7. Smoke: register a demo → verify branded PWA + tour + expiry.

---

## 10. TPS contact (expired landing + reports)

- **Email:** support@tps-egy.com
- **Phone / WhatsApp:** 01020020851 · 01092288093 (WA link `https://wa.me/201020020851`)
- **Website:** https://tps-egy.com

---

## 11. Build sequence (milestones)

1. **M1+M2** VetOneDemo register + provisioner (business, brand, entitlements, roles, 3 doctors w/ slots, staff by role, wire SOAP/medicine/AI-prompt seeders). *Deliverable: register → log into a populated branded tenant.*
2. **M5** 48h expiry (columns, `EnsureDemoActive`, `/demo-expired` with TPS block, `demo:purge`).
3. **M4** onboarding gate (WA QR + Groq, skippable, AI-off-if-skipped).
4. **GuidedTour** module (Shepherd, DB steps, protected media, authoring UI, launcher).
5. **M6** staff PWA runtime branding (+ rebuild).
6. **ActivityLog enhancement** (incidents + heat-map + trial report, host-only).
7. **M7** enable PortalApi + package code+DB+seeders + runbook.

Every new/changed module bumps its own `module_version` in the same commit (Update button appears on deploy copies).

---

## 12. Resolved confirmations

All locked (2026-07-09):
1. **TPS website URL** — `https://tps-egy.com`.
2. **Default doctor count** when blank — **3**.
3. **Purge retention** after expiry — **14 days**.
4. **Tour scope** — **full end-to-end walkthrough** (appointments → SOAP → prescriptions → owner PWA → settings).

Spec is approved-pending-final-review; build proceeds on branch `feat/vetone-demo`.
