# Module Scope — WhatsApp Ticketing System (CodeIgniter 4 Rebuild)

**Source system:** `wa_support_node_v1` (Node.js Fastify API + React SPA + Baileys worker + PostgreSQL)  
**Target:** CodeIgniter 4 module (controllers, models, services, filters, migrations)  
**Document version:** 2026-06-04  
**Companion docs:** `docs/TestSprite_PRD_WA_Ticketing.md`, `docs/PRD_v2_WhatsApp_Ticketing_System.md`, `docs/scope_wa_ticketing_system.html`

---

## 1. Purpose of this document

This scope captures **all implemented functionality**, **business rules**, **data contracts**, and **integration boundaries** of the current ticketing platform so you can rebuild it as a **CI4 module** (new controllers, models, services, views or API-only layer). It is specification-first: do not invent features beyond what is listed unless explicitly marked *deferred* or *out of scope*.

**Suggested CI4 module name:** `WaTicketing` (or `SupportDesk`)  
**Suggested route prefix:** `/api/v1/wa-ticketing/...` or integrate under existing app routes per your CI4 app structure.

---

## 2. Product summary

| Aspect | Detail |
|--------|--------|
| **Product** | Self-hosted, multi-channel customer support / ticketing (WhatsApp primary; also web portal chat and email inbound) |
| **Users** | Internal staff (`admin`, `user`), external company portal users (`company_user`), public signup guests |
| **Core loop** | Inbound message → contact resolve → open ticket find-or-create → persist message → real-time notify staff → optional onboarding / AI / alerts → staff reply outbound |
| **Brand in UI** | Silicon Support |
| **Not in v1 scope of original PRD but implemented** | Email channels, AI agents (multi-provider), knowledge base modules, signup chat, media (image/voice), staff SMS/email alerts, escalation flags |

---

## 3. Suggested CI4 module structure

Map Node services to CI4 layers:

| CI4 layer | Responsibility (from current system) |
|-----------|--------------------------------------|
| **Controllers** | HTTP routes mirroring §10; thin — validate input, call services, return JSON/views |
| **Filters** | JWT auth, `requireAdmin`, `requireStaff`, `requireCompanyUser`, rate limit on signup |
| **Models** | One per table; query builders for list filters |
| **Services** | Business logic currently in `apps/api/src/services/*` |
| **Libraries** | WhatsApp worker client, SMTP, Twilio, Turnstile, LLM providers, Supabase Storage (or CI4 filesystem) |
| **Commands** | `spark migrate`, seed admin, restore WA sessions (if worker stays external) |
| **Events** | Socket.IO equivalent (CI4 may use Pusher, Mercure, or poll — see §12) |

**Controllers to plan (minimum):**

- `AuthController`, `TicketsController`, `ContactsController`, `ChannelsController`
- `UsersController`, `CompaniesController`, `PortalController`
- `KnowledgeModulesController`, `AiAgentsController`, `SettingsController`
- `AuditLogsController`, `SignupController`, `InboundEmailController`
- `InternalController` (worker-only, not public)
- `HealthController` (or use existing app health)

---

## 4. User roles and authorization matrix

### 4.1 Roles

| Role | DB value | Description |
|------|----------|-------------|
| Administrator | `admin` | Full system control |
| Staff agent | `user` | Day-to-day tickets; no user/audit admin |
| Company portal user | `company_user` | Customer org; scoped to `company_id` |
| Guest | *(no JWT)* | Signup flows + health + email webhook (token) only |

### 4.2 Permission matrix

| Capability | admin | user (staff) | company_user |
|------------|:-----:|:------------:|:------------:|
| Dashboard (all tickets) | ✓ | ✓ | Own company only |
| Ticket list / detail / reply | ✓ | ✓ | Own company only |
| Close / priority / assign / escalation | ✓ | ✓ | — |
| Contacts list / detail / link company | ✓ | ✓ | — |
| Channels list / detail / settings | ✓ | View + settings edit | — |
| Channels add / delete / reconnect | ✓ | — | — |
| Companies CRUD + portal users + KB toggles | ✓ | ✓ | — |
| Knowledge modules CRUD | ✓ | ✓ | — |
| AI agents CRUD | ✓ | — | — |
| Set company default AI agent | ✓ | ✓ | — |
| User management | ✓ | — | — |
| Audit log | ✓ | — | — |
| Platform OpenAI settings | ✓ | — | — |
| Portal send message (+ media) | — | — | ✓ |
| Public signup | — | — | — (guest) |

### 4.3 Authorization rules (must implement)

1. **JWT required** on all protected routes; `401` if missing/invalid/expired.
2. **`403`** if role insufficient for route.
3. **`company_user` ticket access:** ticket is allowed only if `contacts.company_id = users.company_id` for that ticket. Staff (`admin`/`user`) see all tickets.
4. **`company_user` without `company_id`:** deny portal and ticket access (`403`).
5. **Portal synthetic contacts** (`phone_number` like `portal:{uuid}`): staff cannot PATCH company link; managed via portal user lifecycle.
6. **Email contacts** (`phone_number` like `email:{address}`): normal staff linking rules apply.
7. **API keys never returned in full** — only `has_api_key` and `api_key_hint` (last 4 chars).
8. **Audit logs:** insert-only; no update/delete API.
9. **Deactivated users** (`is_active = false`): login rejected.
10. **CORS:** restrict to configured origins in production.

### 4.4 Frontend route guards (reference for CI4 views)

| Route pattern | Guard |
|---------------|-------|
| `/login`, `/signup/complete` | Public |
| `/`, `/tickets`, `/tickets/:id` | Authenticated |
| `/contacts`, `/channels`, `/companies`, `/knowledge-base`, `/settings` | Staff only |
| `/admin/*` | Admin only |
| Portal users | Redirect staff routes to `/` |

---

## 5. Feature modules and business rules

### 5.1 Authentication (`authService`)

**Endpoints:** `POST /auth/login`, `POST /auth/logout`, `GET /auth/me`

| Rule | Detail |
|------|--------|
| Login | Email + password; bcrypt verify; issue JWT (`sub`, `email`, `role`, `company_id` for portal users) |
| JWT expiry | Configurable `JWT_EXPIRY` (default 8h) |
| Logout | Client clears token; server may log audit |
| Me | Returns user row without password; `404` if user deleted |
| Audit | Log login success/failure |

**Seed default admin:** `admin@example.com` / `Admin123!` (when using project seed).

---

### 5.2 Tickets (`ticketService`, `messagingService`, `ticketAccessService`)

**Purpose:** Core support ticket lifecycle and messaging.

#### Ticket lifecycle

| State | Meaning |
|-------|---------|
| `open` | Accepts new messages |
| `closed` | No new messages (`400` on send) |

| Rule | Detail |
|------|--------|
| Open ticket lookup | Per `(contact_id, channel_id)` — at most one open ticket; append to it |
| New ticket | Created when no open ticket exists for pair |
| After close | Next inbound creates a **new** ticket (not reopen) |
| List sort | `updated_at DESC` (bumped on every message) |
| Filters | `status`, `priority`, `channel_id`, `assigned_to`, `from`, `to` (dates), `limit`, `offset` |
| Priority | `low`, `medium`, `high` |
| Assignment | `assigned_to` → `users.id` |
| Due date | Optional `due_date` |
| Escalation | `needs_escalation` boolean; set by alerts or manual PATCH |
| Intake | JSON `intake.step` + `intake.answers` for onboarding (see §5.8) |
| Signup link | Optional `signup_session_id` on ticket for WA inline signup |

#### Messaging

| Rule | Detail |
|------|--------|
| Direction | `inbound` \| `outbound` |
| Source | `whatsapp` \| `portal` \| `email` |
| Sequence | Monotonic per ticket (`sequence` auto-increment) |
| Dedup | Unique `wa_message_id` when present (WhatsApp + idempotent email `message_id`) |
| Closed ticket | `400` on any new message |
| Email ticket media | `400` — text only on email channel |
| Text required | Empty content rejected unless multipart media present |
| Media limit | Max **5** files per request; max **50MB** per file (`MEDIA_MAX_BYTES`) |
| Media types | `text`, `image`, `voice_note` |
| Storage | Private bucket `ticket-media`; signed URLs for display (TTL default 3600s) |

#### Outbound by channel

| Channel kind | Outbound path |
|--------------|---------------|
| WhatsApp | Append message → call Baileys worker → store `wa_message_id` |
| Portal | Append with `source: portal` only (no WA send) |
| Email | SMTP send + append with `source: email` |

#### Close ticket side effects

If channel `settings.ticketClosing.enabled` and message non-empty:

- **Portal:** outbound closing message in thread
- **Email:** SMTP to contact address (from `email:` key)
- **WhatsApp:** send via worker, store message

Audit: `ticket.closed`, plus channel-specific closing actions or `ticket.closing_delivery_failed`.

---

### 5.3 Contacts (`contactService`)

**Purpose:** Unified customer identity across channels.

| Rule | Detail |
|------|--------|
| Phone key | E.164 for WhatsApp; `portal:{userUuid}` for portal; `email:{lowercase@addr}` for email |
| Unique | `contacts.phone_number` unique |
| findOrCreate | On every inbound |
| Company link | Staff PATCH `company_id` on contact; upsert company by name during onboarding |
| Portal contact | `ensurePortalUser(userId, companyId)` creates/links `portal:*` row |
| List enrichment | Portal rows may include `portal_company_name`, `portal_user_email`, `portal_user_display_name` |

---

### 5.4 Channels (`channelService`)

**Kinds:** `whatsapp` \| `portal` \| `email`

| Kind | `phone_number` | Baileys | Notes |
|------|----------------|---------|-------|
| whatsapp | E.164 | Yes | QR pairing, reconnect, session restore |
| portal | `portal:system` | **Never** | Synthetic Web Portal channel; always "connected" |
| email | N/A | **Never** | `inbound_token` UUID for webhook URL |

#### WhatsApp channel

| Rule | Detail |
|------|--------|
| Create | `POST /channels/init` — `{ name, phone_number }` → status `connecting` → start worker session |
| QR | Worker emits `channel:qr` (Socket.IO); UI displays QR |
| Connected | `channel:connected` → status `connected` |
| Reconnect | Admin only; clears worker session → `connecting` → new session; `503` if worker down |
| Delete | Admin only; terminates session |
| Startup | `restoreAllSessions` on API boot (non-fatal if DB down) |
| Disconnect | Audit `channel.disconnected`; status update |

#### Email channel

| Rule | Detail |
|------|--------|
| Create | `POST /channels/email` — `{ name }` → returns `inbound_token` (secret) |
| Inbound | `POST /inbound/email/:token` — public, token auth |
| List API | **Never** expose `inbound_token` in channel list |
| Reconnect | `400` — not applicable |

#### Web portal channel

| Rule | Detail |
|------|--------|
| Singleton | `portal:system` seeded / always on |
| Settings inheritance | Onboarding / AI / ticket closing **shown in API** are copied from **oldest WhatsApp channel** by `created_at` (`web_chat_settings_source`) |
| Reconnect / Baileys | `400` if attempted |

#### Channel settings JSON (`settings` column, merged with defaults)

```text
settings.onboarding.enabled + templates (whoAreYou, company, query, queryStatus)
settings.ai.enabled, model, systemPrompt, knowledgeBaseText
settings.ticketClosing.enabled, message
settings.alerts.sms (enabled, phone_e164)
settings.alerts.email (enabled, address)
settings.alerts.escalation (enabled, emailThenSms, flagTicketForEscalation)
```

Admin PATCH validates onboarding template fields non-empty when onboarding enabled.

---

### 5.5 Companies (`companyService`)

| Rule | Detail |
|------|--------|
| Create | `{ name }` → `normalized_name` unique; generates `account_code` (e.g. `C` + hex) for join flow |
| AI autoreply | `ai_autoreply_enabled` gates talkback for contacts linked to company |
| Portal users | `POST /companies/:id/users` — creates `company_user` with email/password/display_name |
| Knowledge | `PUT .../knowledge-modules` — `{ module_ids: uuid[] }` junction table |
| Default AI agent | `PUT .../default-ai-agent` — `{ ai_agent_id: uuid \| null }` |
| Detail aggregates | Linked contacts, portal users, tickets (limit ~200), channel numbers from ticket activity |

---

### 5.6 Portal messaging (`portalService`)

**Endpoint:** `POST /api/v1/portal/messages` — `company_user` only

| Rule | Detail |
|------|--------|
| Content | Text required for JSON; multipart supports media (not on email tickets) |
| Ticket | Optional `ticketId`; else find open on portal channel or create |
| Ownership | Ticket must belong to user's portal contact + portal channel |
| Closed | `400` |
| Flow | Same talkback + alerts as WhatsApp after inbound (uses portal channel settings / inherited WA settings) |
| Audit | `portal.message` |

---

### 5.7 Inbound email (`inboundEmail` route)

**Endpoint:** `POST /api/v1/inbound/email/:token`

| Rule | Detail |
|------|--------|
| Auth | URL token matches `channels.inbound_token` |
| Body | `{ from, text, subject?, from_name?, message_id? }` required: `from`, `text` |
| Contact | `email:{normalized@address}` |
| Dedup | Skip if `message_id` already seen |
| Talkback | If channel AI/onboarding configured |
| Alerts | `notifyInboundAlerts` |

---

### 5.8 Onboarding (intake) — `talkbackService.nextOnboardingStep`

**When:** Channel `settings.onboarding.enabled` AND **first ticket ever** for `(contact, channel)` → intake starts at `step: idle`.

**Steps (sequential prompts on inbound):**

| Step | Prompt template key | Stores answer from inbound |
|------|---------------------|----------------------------|
| `idle` → `who` | whoAreYou | — (sends prompt) |
| `who` → `company` | company | whoAreYou |
| `company` → `query` | query | company |
| `query` → `status` | queryStatus | query |
| `status` → `done` | — | queryStatus |

| Rule | Detail |
|------|--------|
| Returning customers | Second+ ticket on same channel: intake `step: done` immediately |
| Empty template | If template text empty, step does not advance (warn in logs) |
| Company name | If answer provided, `upsertByName` + `linkContactToCompany` |
| AI | Runs only when `intake.step === 'done'` |
| Same inbound | If onboarding sent a template this turn, **skip AI** on that turn |

---

### 5.9 AI auto-reply (talkback) — `talkbackService`, `llmProviderService`, `aiAgentService`

**Trigger:** After inbound (WA / portal / email), intake complete, channel `settings.ai.enabled`.

| Gate | Condition |
|------|-----------|
| Contact linked | `contacts.company_id` must be set |
| Company flag | `companies.ai_autoreply_enabled = true` |
| Knowledge | At least one enabled module for company (respecting agent scope) |
| LLM credentials | Company default agent with key **OR** platform/env OpenAI key |

**Credential precedence for LLM:**

1. Company default `ai_agents` row (if `api_key` set) → `completeChat` with agent persona + guardrails
2. Else channel AI settings + `chatCompletionText` / platform OpenAI

**Knowledge injection:**

- Channel KB text capped (~6000 chars)
- Company modules concatenated (markdown), ordered by `sort_order`, total budget ~12k
- Agent scoped modules: intersection of `ai_agent_knowledge_modules` and `company_knowledge_modules`
- System guardrail: answer only from KB; refuse off-topic

**Providers:** `openai`, `anthropic`, `openai_compatible` (custom base URL)

**Failure:** Log `talkback.ai_failed`; no user-facing crash; optional no reply

**Automated outbound:** `sendAutomatedOutbound` — same paths as staff reply per channel kind

---

### 5.10 Knowledge base (`knowledgeModuleService`)

| Rule | Detail |
|------|--------|
| CRUD | Staff only |
| Fields | `slug`, `title`, `group_label`, `body_markdown`, `sort_order` |
| Company enablement | Junction `company_knowledge_modules` |
| Talkback | Builds markdown blob for enabled modules |

---

### 5.11 AI agents (`aiAgentService`)

| Rule | Detail |
|------|--------|
| CRUD | Admin only (create/update/delete); staff can list/get |
| Fields | `display_name`, `provider`, `api_key`, `base_url`, `model_id`, `persona_system`, `guardrails`, `knowledge_module_ids[]` |
| Company default | One row in `company_ai_agents` per company |
| Staff link | Optional `users.ai_agent_id` (UX; talkback uses company default) |
| Preview models | `POST /ai-agents/preview-models` — credentials in body, not query string |
| Masking | Never return full `api_key` |

---

### 5.12 Staff alerts (`channelAlertService`)

On inbound message (all channel types):

| Alert type | Config | Behavior |
|------------|--------|----------|
| Email | `settings.alerts.email` | SMTP to staff address |
| SMS | `settings.alerts.sms` | Twilio to E.164 |
| Escalation order | `escalation.emailThenSms` | If escalation enabled: sequential email→SMS vs parallel |
| Escalation flag | `flagTicketForEscalation` | Sets `tickets.needs_escalation = true` |

Audit: `channel.alert_email_sent|failed`, `channel.alert_sms_sent|failed`

---

### 5.13 Public signup (`signupService`)

**Routes:** `POST /signup/chat`, `GET /signup/verify`, `POST /signup/complete`  
**Rate limit:** 40 requests/minute per IP

**Chat state machine (`signup_sessions`):**

| Step | Collects |
|------|----------|
| `intent` | `new` company vs `join` existing (account code) |
| `company_new` | New company display name |
| `company_join` | Account code → resolve `company_id` |
| `email` | Unique email |
| `phone` | Phone number |
| `display_name` | Display name |
| `confirm` | Affirmative → send verification email |
| `emailed` | Awaiting link click |

| Rule | Detail |
|------|--------|
| Session TTL | 24 hours |
| Token | SHA-256 hash stored; plain token in email link only |
| Turnstile | Required on **first** message when `TURNSTILE_SECRET_KEY` set |
| Complete | `POST /signup/complete` `{ token, password }` → creates `company_user` + company if new |
| Verification link | Built from `FRONTEND_URL` |
| Duplicate email | Rejected during chat |
| Audit | `signup.verification_sent`, `signup.completed` |

**Frontend routes:** `/signup/complete` (lazy-loaded); signup chat may be embedded on login page.

**WhatsApp inline signup:** `waSignupTalkbackService` — ties signup session to ticket during WA conversation (see ticket `signup_session_id`).

---

### 5.14 Platform settings (`platformSettingsService`)

| Rule | Detail |
|------|--------|
| Singleton | `platform_settings` row |
| Admin GET | `has_openai_key`, `openai_key_source`, `openai_base_url` — no full key |
| Admin PATCH | `openai_api_key`, `openai_base_url` |
| Models list | `GET /settings/openai/models` — admin only |

---

### 5.15 Audit log (`auditService`)

| Rule | Detail |
|------|--------|
| Immutable | Insert only |
| Actor | `user` (actor_id) or `system` (null actor) |
| Admin list | Filters: actor, action, entity_type, date range |

**Action catalog (implement logging for parity):**

- Auth: login success/failure  
- Users: `user.created`, `user.role_changed`, `user.deactivated`  
- Channels: `channel.init_started`, `channel.email_created`, `channel.added`, `channel.updated`, `channel.removed`, `channel.reconnect_started`, `channel.connected`, `channel.disconnected`  
- Tickets: `ticket.created`, `ticket.closed`, `ticket.priority_changed`, `ticket.due_date_set`, `ticket.assigned`, `ticket.escalation_flag`, closing variants  
- Messages: `message.received`, `message.sent`, `message.send_failed`, `message.automated`, `message.automated_failed`  
- Portal: `portal.message`  
- Talkback: `talkback.ai_failed`  
- Alerts: `channel.alert_email_sent|failed`, `channel.alert_sms_sent|failed`  
- Knowledge: `knowledge_module.created|updated|deleted`  
- AI agents: `ai_agent.created|updated|deleted`  
- Platform: `platform.settings_updated`  
- Signup: `signup.verification_sent`, `signup.completed`  

---

### 5.16 Internal / WhatsApp worker API (`internalRoutes`)

**Not public.** Protect with internal secret or private network.

| Endpoint | Purpose |
|----------|---------|
| `POST /internal/messages` | Inbound from Baileys (text or media base64) |
| Worker callbacks | Channel connected/disconnected events |

**Inbound processing order:**

1. Validate body; dedup `waId`  
2. findOrCreate contact  
3. findOrCreate open ticket (onboarding intake if first on channel)  
4. Append message (+ upload media if present)  
5. Emit `message:new`; `ticket:created` if new  
6. `waSignupTalkbackService` if signup mode  
7. `talkbackService.handleInbound`  
8. `notifyInboundAlerts`  

**Body limit:** ~58MB for base64 media.

---

## 6. Database schema (PostgreSQL → CI4 migrations)

Apply in order (19 migrations in source repo). Use `$this->db->prefixTable()` in CI4 if sharing DB with prefixed tables.

### 6.1 Tables

| Table | Purpose |
|-------|---------|
| `users` | Staff + portal users |
| `contacts` | Customer identities |
| `channels` | WA / portal / email |
| `tickets` | Support tickets |
| `messages` | Ticket thread |
| `audit_logs` | Immutable audit |
| `companies` | Customer organisations |
| `signup_sessions` | Signup chat state |
| `platform_settings` | Global OpenAI config |
| `knowledge_modules` | KB articles |
| `company_knowledge_modules` | Company ↔ module |
| `ai_agents` | LLM agent configs |
| `company_ai_agents` | Company default agent |
| `ai_agent_knowledge_modules` | Agent KB scope |

### 6.2 Key columns and enums

**users:** `id`, `email` (unique), `password_hash`, `role` (`admin`|`user`|`company_user`), `company_id`, `display_name`, `phone`, `ai_agent_id`, `is_active`, `created_at`, `last_login_at`

**contacts:** `id`, `phone_number` (unique, varchar 255), `display_name`, `company_id`

**channels:** `id`, `name`, `phone_number`, `kind`, `status` (`connecting`|`connected`|`disconnected`), `inbound_token` (email), `settings` (JSONB), `session_data` (JSONB, WA), timestamps

**tickets:** `id`, `contact_id`, `channel_id`, `assigned_to`, `status`, `priority`, `due_date`, `needs_escalation`, `intake` (JSONB), `signup_session_id`, `created_at`, `updated_at`, `closed_at`

**messages:** `id`, `ticket_id`, `direction`, `content`, `source`, `message_type`, `wa_message_id` (unique nullable), `sequence`, media columns, timestamps

**companies:** `id`, `name`, `normalized_name` (unique), `account_code` (unique), `ai_autoreply_enabled`

### 6.3 Indexes and constraints

- FK on all relationships  
- `(contact_id, channel_id, status)` for open-ticket lookup  
- `messages.wa_message_id` unique where not null  
- `channels.inbound_token` unique where not null  
- Soft delete users via `is_active` only  

### 6.4 Retention

- Messages, tickets, audit: **no deletion** in app  
- WA session files: Docker volume `wa_auth` (if worker remains separate)  
- Media: object storage bucket `ticket-media`  

---

## 7. API contract summary (v1)

Base: `/api/v1`  
Error format: `{ error: string }` with HTTP codes 400, 401, 403, 404, 409, 413, 503, 500

| Group | Methods | Role guard |
|-------|---------|------------|
| Auth | login, logout, me | Public / any |
| Tickets | list, get, patch, post messages | Auth + ticket access |
| Contacts | list, get, patch | Staff |
| Channels | list, get, init, email, patch, delete, reconnect | Mixed (admin for mutate) |
| Users | list, create, patch, delete | Admin |
| Companies | CRUD, users, knowledge, default agent | Staff |
| Knowledge modules | CRUD | Staff |
| AI agents | CRUD, preview-models | Admin mutate / staff read |
| Portal | post messages | company_user |
| Inbound email | POST by token | Public (token) |
| Signup | chat, verify, complete | Public (rate limited) |
| Settings | platform GET/PATCH, openai models | Admin |
| Audit logs | list | Admin |
| Internal | messages, status | Worker secret |
| Health | GET | Public |

Full request/response shapes: see `docs/TestSprite_PRD_WA_Ticketing.md` §7.

---

## 8. Real-time events (Socket.IO)

**Current:** JWT in handshake (`auth.token` or `query.token`).

### Server → client

| Event | Payload | When |
|-------|---------|------|
| `channel:qr` | `{ channelId, qrDataUrl }` | WA pairing |
| `channel:connected` | `{ channelId }` | WA linked |
| `channel:disconnected` | `{ channelId, reason }` | Session lost |
| `message:new` | `{ ticketId, message }` | Inbound/outbound appended |
| `ticket:created` | `{ ticket }` | New ticket |
| `ticket:updated` | `{ ticket }` | Metadata/intake change |
| `message:sent` | `{ messageId, status, error? }` | Outbound delivery result |

### Client → server

| Event | Payload |
|-------|---------|
| `tickets:subscribe` | `{ ticketIds[] }` — company_user filtered by access |
| `tickets:unsubscribe` | `{ ticketIds[] }` |

**CI4 note:** Replace with your stack (e.g. Mercure, Pusher, SSE, or short polling). Preserve event names/payloads if keeping the React UI.

---

## 9. UI screens inventory (for parity or CI4 views)

| Screen | Route | Roles |
|--------|-------|-------|
| Login | `/login` | Public |
| Signup complete | `/signup/complete` | Public |
| Dashboard | `/` | All authenticated |
| Ticket list | `/tickets` | All (scoped) |
| Ticket detail | `/tickets/:id` | All (scoped) |
| Contacts | `/contacts` | Staff |
| Contact detail | `/contacts/:id` | Staff |
| Channels | `/channels` | Staff |
| Channel detail | `/channels/:id` | Staff |
| Companies | `/companies` | Staff |
| Company detail | `/companies/:id` | Staff |
| Knowledge base | `/knowledge-base` | Staff |
| Module edit | `/knowledge-base/:id` | Staff |
| Settings | `/settings` | Staff (platform OpenAI: admin) |
| Users | `/admin/users` | Admin |
| User detail | `/admin/users/:id` | Admin |
| AI agents | `/admin/ai-agents` | Admin |
| Audit log | `/admin/audit-logs` | Admin |

**UX requirements:** Mobile ≥375px; loading states; toasts/errors; real-time thread updates; media preview/playback; ticket sidebar (intake, escalation, metadata).

---

## 10. External integrations and environment

| Integration | Env vars (typical) | Used for |
|-------------|-------------------|----------|
| PostgreSQL | `DATABASE_URL` | All persistence |
| JWT | `JWT_SECRET`, `JWT_EXPIRY` | Auth |
| WhatsApp worker | `WHATSAPP_WORKER_URL`, internal secret | Send/receive WA |
| SMTP | `SMTP_*` | Email outbound, alerts, signup |
| Twilio | Twilio creds in env | SMS alerts |
| OpenAI | `OPENAI_API_KEY`, platform DB | AI talkback |
| Anthropic / compatible | Per-agent keys | AI talkback |
| Supabase Storage | Supabase URL + service key | Ticket media |
| Turnstile | `TURNSTILE_SECRET_KEY`, site key on FE | Signup captcha |
| CORS | `CORS_ORIGIN` | Browser API access |
| Frontend URL | `FRONTEND_URL`, `PUBLIC_SITE_HOST` | Links, CORS |

---

## 11. Security requirements (mandatory for CI4 module)

- bcrypt passwords  
- JWT on protected routes  
- Rate limit signup (40/min/IP)  
- Turnstile on signup when configured  
- Captcha on auth routes per org standards (extend CI4 auth if required)  
- Rate limit all API endpoints in production  
- Row-level company isolation for `company_user`  
- Never expose full API keys or `inbound_token` in list APIs  
- Validate MIME and size on uploads  
- Audit all sensitive actions  
- CORS restricted in production  

---

## 12. CI4 rebuild decisions (architectural)

Document these choices in your implementation plan:

| Topic | Current (Node) | CI4 options |
|-------|----------------|-------------|
| WhatsApp | Separate Baileys worker service | Keep worker + `InternalController` **or** PHP WA library (higher risk) |
| Real-time | Socket.IO on same process | Mercure / Pusher / polling |
| Media storage | Supabase Storage signed URLs | CI4 filesystem + signed routes, or keep Supabase SDK |
| Auth | Custom JWT | CI4 Shield / Sanctum-style JWT / session |
| Frontend | React SPA | Keep SPA consuming CI4 API **or** CI4 views + HTMX |
| Migrations | SQL files | CI4 `php spark migrate` |
| Multi-tenant | Single DB, company_id scoping | Same model |

**Recommended minimal path:** CI4 REST API module + keep React frontend + keep Baileys worker (HTTP internal only) → smallest behavioral risk.

---

## 13. Out of scope (do not rebuild unless product asks)

- Group WhatsApp chats  
- WhatsApp voice/video calls  
- POP3/IMAP email polling (webhook only)  
- Multi-language UI  
- MFA / 2FA  
- Billing / payments  
- CSAT surveys  
- Visual chatbot builder  
- Native mobile apps  
- CSV/PDF export  
- Scheduled messages  
- Canned responses UI  

---

## 14. Acceptance criteria checklist (module complete)

Use as Definition of Done for the CI4 module:

- [ ] All roles and matrix in §4 enforced on every route  
- [ ] Ticket find-or-create + close + message rules in §5.2  
- [ ] Three channel kinds with correct constraints in §5.4  
- [ ] Onboarding state machine §5.8 on first ticket per contact/channel  
- [ ] AI talkback gates and credential chain §5.9  
- [ ] Portal isolation §5.6  
- [ ] Email inbound webhook §5.7  
- [ ] Signup flow §5.13 with rate limit and Turnstile  
- [ ] Media upload limits and signed URLs §5.2  
- [ ] Staff alerts §5.12  
- [ ] Audit actions §5.15  
- [ ] API keys masked §4.3  
- [ ] Internal worker API secured §5.16  
- [ ] Real-time or documented polling replacement §8  
- [ ] PHPUnit coverage for services listed in TestSprite PRD §11  

---

## 15. Source code map (Node → CI4 service names)

| Node service file | CI4 service class (suggested) |
|-------------------|-------------------------------|
| `authService.ts` | `AuthService` |
| `ticketService.ts` | `TicketService` |
| `messagingService.ts` | `MessagingService` |
| `contactService.ts` | `ContactService` |
| `channelService.ts` | `ChannelService` |
| `companyService.ts` | `CompanyService` |
| `portalService.ts` | `PortalService` |
| `signupService.ts` | `SignupService` |
| `talkbackService.ts` | `TalkbackService` |
| `aiAgentService.ts` | `AiAgentService` |
| `knowledgeModuleService.ts` | `KnowledgeModuleService` |
| `platformSettingsService.ts` | `PlatformSettingsService` |
| `ticketMediaService.ts` | `TicketMediaService` |
| `channelAlertService.ts` | `ChannelAlertService` |
| `emailService.ts` | `EmailService` |
| `twilioSmsService.ts` | `TwilioSmsService` |
| `captchaService.ts` | `CaptchaService` |
| `auditService.ts` | `AuditService` |
| `ticketAccessService.ts` | `TicketAccessService` |
| `automatedOutboundService.ts` | `AutomatedOutboundService` |
| `llmProviderService.ts` | `LlmProviderService` |
| `waSignupTalkbackService.ts` | `WaSignupTalkbackService` |
| `messageEmitService.ts` | `RealtimeEmitter` (adapter) |

Shared types/enums: port from `packages/shared/src/types.ts` to PHP enums/DTOs.

---

## 16. Related files in this repo

| Path | Use |
|------|-----|
| `docs/TestSprite_PRD_WA_Ticketing.md` | Full PRD, test requirements, API detail |
| `docs/PRD_v2_WhatsApp_Ticketing_System.md` | Architecture and milestone context |
| `supabase/migrations/*.sql` | Canonical schema |
| `packages/shared/src/types.ts` | Types, defaults, merge helpers |
| `apps/api/src/routes/*.ts` | Route → controller mapping |
| `apps/api/src/services/*.ts` | Business rules |
| `apps/frontend/src/App.tsx` | UI routes |
| `testsprite_tests/testsprite_frontend_test_plan.json` | E2E scenario list |

---

*End of module scope — WhatsApp Ticketing System for CodeIgniter 4 rebuild.*
