# Customer Portal — Improvement & Growth Plan

> Package: capell-app/customer-portal · Kind: package · Tier: premium · Product group: Capell Content · Bundle: content-product · Status: Draft

## 1. Snapshot

Customer Portal is the authenticated self-service foundation for Capell sites. It exposes two surfaces: a **frontend** dashboard (3 routes under `/portal` behind `web,auth` — `ShowCustomerPortalController`, `UpdatePortalPreferencesController`, `StorePortalSupportRequestController`) and an **admin** Filament triage resource (`PortalSupportRequestResource`). Domain logic lives in 6 Actions (`FindOrCreatePortalAccountAction`, `ResolveAuthenticatedPortalAccountAction`, `ResolvePortalDashboardItemsAction`, `ResolvePortalSelfServiceItemsAction`, `SubmitSupportRequestAction`, `UpdatePortalPreferencesAction`, `UpdateSupportRequestStatusAction`). It owns two tables (`portal_accounts`, `portal_support_requests`) via models `PortalAccount` and `PortalSupportRequest`, with encrypted PII columns and HMAC email hashing. Extensibility runs through four provider registries (dashboard items, self-service items, profile, preferences); deps are `capell-app/core` + `capell-app/admin`, with `supports` for access-gate, document-lifecycle, events, newsletter, payments (each of which registers self-service/dashboard providers). 1804 LOC across 41 PHP files. Current marketplace summary: **"A unified, logged-in customer hub for Capell sites — payments, documents, event registrations, gated content and support requests in one secure dashboard."** Screenshots: **4 promoted light-mode Capell runner PNGs** plus committed dark variants cover the frontend dashboard, preferences/support form, admin triage list, and admin triage detail.

## 2. Improvements (existing functionality)

- **Shipped 2026-06-03: health check body.** `CustomerPortalHealthCheck` now probes both required tables and verifies `PortalAccount` / `PortalSupportRequest` model resolution, with coverage in `CustomerPortalHealthCheckTest`. — **S**
- **Shipped 2026-06-03: admin triage site scoping.** `PortalSupportRequestResource::getEloquentQuery()` applies `SiteScope::applyForCurrentActor()` so non-global admins only see assigned-site support requests, with multi-site coverage in `PortalSupportRequestResourceScopeTest`. — **S**
- **Shipped 2026-06-04: preference schema drives rendering and validation.** `ResolvePortalPreferenceOptionsAction` reads `capell-customer-portal.preferences`, the dashboard loops over the resolved options, and `UpdatePortalPreferencesController` normalizes only configured keys.
- **Done/Shipped: cap dashboard provider fan-out.** `ResolvePortalDashboardItemsAction` and `ResolvePortalSelfServiceItemsAction` now cap each provider before merging and apply a global cap after sorting, using `dashboard_items_per_provider_limit`, `dashboard_items_limit`, `self_service_items_per_provider_limit`, and `self_service_items_limit`. This keeps the dashboard bounded when payments, documents, events, newsletter, access-gate, and support all contribute items; dedicated "view all" routes remain part of the later support/billing/download depth rather than dead links. — `src/Actions/ResolvePortalSelfServiceItemsAction.php`, `src/Actions/ResolvePortalDashboardItemsAction.php`, `config/capell-customer-portal.php` — **M**
- **Validate support requests in one place** — `StorePortalSupportRequestController` validates `subject`/`message`, then `SubmitSupportRequestAction` re-trims and re-throws `ValidationException` for empty values. The Action's checks are unreachable from the HTTP path (already `required`) and duplicate intent. Consolidate on a FormRequest or keep the guard only in the Action and thin the controller. — `src/Actions/SubmitSupportRequestAction.php`, `src/Http/Controllers/StorePortalSupportRequestController.php` — **S**
- **Make the `noStore()` header order intentional** — the controller sets `Cache-Control: private, no-store` but the test asserts `no-store, private`; it passes only because Symfony's `ResponseHeaderBag` re-orders directives. This is fragile coupling to framework internals for a `sensitiveOutput: true` package. Assert the normalized value deliberately (or set directives so order is explicit) and add a regression note. — `src/Http/Controllers/ShowCustomerPortalController.php`, `tests/Feature/Frontend/CustomerPortalFrontendTest.php` — **S**
- **Reuse the priority/status option helpers** — `ShowCustomerPortalController::priorityOptions()` and `PortalSupportRequestResource::priorityOptions()/statusOptions()` independently re-map the same enums. Extract a single helper on the enum or a Data object. — `src/Http/Controllers/ShowCustomerPortalController.php`, `src/Filament/Resources/PortalSupportRequests/PortalSupportRequestResource.php` — **S**

## 3. Missing Features (gaps)

Tied to `capabilities[]` and customer-portal table stakes:

- **`portal-profile` capability is unreachable (table stakes, broken)** — `capabilities` advertises `portal-profile` and `portal-preferences`, and `PortalProfileProviderRegistry` / `PortalPreferencesProviderRegistry` are bound as singletons in `CustomerPortalServiceProvider`. But there is **no `ResolvePortalProfileAction` / `ResolvePortalPreferencesAction`**, the dashboard reads raw `$portalAccount->profile` / `preferences` columns directly, and a monorepo-wide grep finds **zero implementers** of `PortalProfileProvider` / `PortalPreferencesProvider`. Account-profile self-service (name, contact, addresses) is the most basic customer-portal feature and is currently absent end-to-end. Wire a resolve Action + render a profile section, or drop the dead registries. — table stakes.
- **Done/Positioned: authentication is a BYO host-app boundary.** The package intentionally mounts behind the configured authenticated middleware stack and resolves the current Laravel user into a site-scoped `PortalAccount`; it does not install login, registration, password reset, magic-link, or SSO screens. README/foundation docs now state this boundary and point buyers to host auth, Fortify, Socialite, Access Gate, or another auth package for the identity journey. — table stakes boundary.
- **Shipped 2026-06-04: support workflow events and requester notifications.** `SubmitSupportRequestAction` now emits `PortalSupportRequestSubmitted`, and `UpdateSupportRequestStatusAction` emits `PortalSupportRequestStatusChanged` only for real status transitions. Both workflows queue requester mail notifications when an email address is available.
- **Done/Shipped: two-way support threading.** `portal_support_request_replies` stores encrypted reply bodies with `sender_type`, optional morph author, submitted timestamp, and encrypted attachment metadata references. Customers can reply from recent support history, replies move tickets to `WaitingOnTeam`, staff can reply from the admin triage table, and team replies move tickets to `WaitingOnCustomer`. Customer Portal intentionally stores attachment references rather than owning file storage/downloads.
- **Done/Positioned: billing, downloads, and entitlements are owning-package surfaces.** `portal-payments-feed`, `portal-document-feed`, and `portal-gated-resource-feed` are intentionally provider-owned: Payments issues invoice, payment-method, checkout, and subscription URLs; Document Lifecycle and gated-resource packages issue signed download or entitlement URLs. Customer Portal aggregates and renders those customer-facing self-service items without importing billing, document, or entitlement internals. — suite boundary.
- **Shipped 2026-06-04: account status enforcement.** `ResolveAuthenticatedPortalAccountAction` now blocks suspended and archived accounts before dashboard, preference, or support workflows run, with frontend coverage for both statuses.
- **No factories or seeders** — neither model ships a factory (`database/factories/` does not exist) though both `use HasFactory`; `PortalAccount::factory()` would throw. Blocks downstream demo/testing and contradicts Capell's "create useful factories" convention. — table stakes for a sellable package.

## 4. Issues / Risks

- **Shipped 2026-06-03: admin cross-tenant support-request leak fixed.** The support-request resource is site-scoped for non-global admins.
- **Shipped 2026-06-03: critical health check is real.** The package health check now fails when required storage is missing.
- **Dead capability `portal-profile` / unused registries (tech debt + manifest mismatch).** Profile & preferences provider registries have no resolver, no consumer, and no implementer anywhere. — `src/Support/PortalProfileProviderRegistry.php`, `src/Support/PortalPreferencesProviderRegistry.php`, `src/Providers/CustomerPortalServiceProvider.php`.
- **Shipped 2026-06-04: frontend render/query budgets.** `capell.json` now declares `frontendRenderBudgetMs: 200` and `frontendQueryBudget: 20`, with manifest tests covering both values.
- **Shipped 2026-06-04: frontend account isolation coverage.** `CustomerPortalFrontendTest` now proves one account cannot see another account's support requests on the same site.
- **No negative-path / authz tests.** Missing: unauthenticated request → 403 (the `abort_unless` in `ResolvesPortalAccount`), throttle on `support.store` (`throttle:12,1`), and broader admin resource gating. Suspended/archived accounts and cross-site admin scope are now covered. — `tests/`.
- **PII handling is good but partial.** `subject`/`message`/`requester_email`/`context` are `encrypted` and emails are HMAC-hashed for lookup (`PortalAccount::emailHash`), but `hash_secret` defaults to `null` in config and falls through to an app secret — document the key-rotation story, and note encrypted columns are unsearchable in admin (the resource has no search, consistent with this, but worth stating). — `config/capell-customer-portal.php`, `src/Models/PortalSupportRequest.php`.
- **`ResourceEnum` is unreferenced (minor dead code).** `src/Enums/ResourceEnum.php` maps one case to the resource class with no consumer found. — `src/Enums/ResourceEnum.php`.
- **Public output safety: OK, with one note.** The dashboard passes hydrated arrays (no DB queries in Blade), sends `noindex` + `no-store`, and the test asserts no package name / `portal_account_id` / `signed` / `Filament` leaks — aligns with Capell public-output rules. Risk: provider-supplied `url`/`description`/`label` strings are rendered with `{{ }}` (escaped) but their provenance is third-party packages; keep them text-only and never `{!! !!}`. — `resources/views/dashboard.blade.php`.
- **i18n: strong.** All user-facing strings use `capell-customer-portal::` translations and enum labels resolve via translation keys. No hardcoded English in templates found. Maintain parity if keys are added for new preferences.
- **No README (docs debt).** The package now has `docs/foundation.md` and `CHANGELOG.md`, but still lacks a root `README.md`. — package root.

## 5. Marketplace & Selling

**Current `summary`:** "Customer Portal gives Capell sites a safe authenticated self-service foundation." — accurate but generic and inward-facing ("foundation" signals unfinished); leads with "safe" (table stakes) rather than buyer value. **Composer `description`:** "Authenticated customer self-service foundation for Capell CMS." — nearly identical, also says "foundation". Both undersell the real, working differentiator: a pluggable dashboard that **aggregates payments, documents, events, gated content and support into one logged-in hub** without those packages knowing about each other.

**Improved 1-sentence summary:** "A unified, logged-in customer hub for Capell sites — payments, documents, event registrations, gated content and support requests in one secure dashboard."

**Improved 3–4 sentence description:** "Customer Portal turns a Capell site into an authenticated self-service experience. A single dashboard aggregates billing, documents, event registrations, gated resources and newsletter preferences contributed by your other Capell packages — no custom glue code, just install and they appear. Customers raise and track support requests; staff triage them from the admin panel with full status workflow and audit-safe, encrypted records. Built on Capell's Action and provider-registry architecture, it stays cache-safe and never leaks admin internals into public output."

**Screenshot / media status:** Done/Shipped. `docs/screenshots.json` now defines four route-backed Capell runner captures: frontend dashboard, preferences + support form, admin triage list, and admin triage edit/detail. `capell.json` promotes the four light PNGs into marketplace media, and dark variants are committed for documentation use.

**Pricing / tier / bundle positioning:** `premium` in the `content-product` bundle is defensible _if_ the dead `portal-profile` capability and notifications land; today the standalone value is thin (it's a shell over host auth + other packages' feeds). Position as the **anchor of an Extension Suite**: it is most valuable when sold with `payments` (billing self-service), `document-lifecycle`/downloads (entitlements), and `events` (registrations). Cross-sell is already wired via the provider registries — lead with "buy the suite, get a portal that lights up."

**Differentiators / value props / target buyer:** Differentiator = zero-glue aggregation via typed provider contracts + encrypted PII + Capell cache-safe rendering. Target buyer = agencies/site owners already running Capell payments/events/documents who want a customer login area without bespoke development. Value props: one hub, install-and-appear integrations, GDPR-friendly encrypted support records, multi-site aware (once admin scoping is fixed).

**Keywords / tags (8–12):** customer portal, self-service, account dashboard, support tickets, member area, authenticated frontend, billing self-service, document downloads, event registrations, preference center, multi-site, CRM. (Current `categories`: admin, frontend, crm, self-service — extend with the above.)

## 6. Prioritized Roadmap

| Item                                                                                                                                                                                                                                                                                       | Bucket  | Effort | Impact | Section ref                                                                                                                                                                    |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | ------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Scope `PortalSupportRequestResource` per site (`getEloquentQuery`)                                                                                                                                                                                                                         | Shipped | S      | High   | §2, §4                                                                                                                                                                         |
| Add tenant-isolation test (account A cannot see B's data)                                                                                                                                                                                                                                  | Shipped | S      | High   | §4                                                                                                                                                                             |
| Implement real health-check probe (tables + models)                                                                                                                                                                                                                                        | Shipped | S      | High   | §2, §4                                                                                                                                                                         |
| Resolve `portal-profile`: add resolve Action + render, or remove dead registries                                                                                                                                                                                                           | Shipped | M      | High   | §3, §4                                                                                                                                                                         |
| Add factories (+ optional seeder) for both models                                                                                                                                                                                                                                          | Shipped | S      | Med    | §3                                                                                                                                                                             |
| Add unauthenticated/throttle/suspended negative-path tests                                                                                                                                                                                                                                 | Shipped | S      | Med    | §4                                                                                                                                                                             |
| Enforce `PortalAccountStatus` (block suspended/archived)                                                                                                                                                                                                                                   | Shipped | S      | Med    | §3                                                                                                                                                                             |
| Emit events + notifications on submit / status change                                                                                                                                                                                                                                      | Shipped | M      | High   | §3                                                                                                                                                                             |
| Set real `frontendRenderBudgetMs` + frontend query budget                                                                                                                                                                                                                                  | Shipped | S      | Med    | §4                                                                                                                                                                             |
| Done/Shipped: Capture marketplace screenshots (4 surfaces) + rewrite summary/description                                                                                                                                                                                                   | Shipped | S      | High   | §5                                                                                                                                                                             |
| Schema-drive preferences (single source for view + validation)                                                                                                                                                                                                                             | Shipped | M      | Med    | §2, §3                                                                                                                                                                         |
| Done/Shipped: Cap / paginate dashboard + self-service provider fan-out                                                                                                                                                                                                                     | Shipped | M      | Med    | §2                                                                                                                                                                             |
| Add README + CHANGELOG                                                                                                                                                                                                                                                                     | Shipped | S      | Med    | §4                                                                                                                                                                             |
| Two-way support threading (replies + attachments)                                                                                                                                                                                                                                          | Shipped | L      | High   | §3 — 2026-06-06: added support replies table/model/action, customer reply route/form, admin reply action, encrypted reply bodies, and encrypted attachment-reference metadata. |
| Done/Positioned: First-party billing & downloads/entitlement surfaces (suite cross-sell). Evidence: README/foundation docs state Payments, Document Lifecycle, and gated-resource packages own their domain operations and contribute self-service URLs through Customer Portal providers. | Shipped | L      | High   | §3, §5                                                                                                                                                                         |
| Done/Positioned: Provide auth/SSO surface (or formally position as BYO-auth). Evidence: README/foundation docs state Customer Portal is a BYO-auth package that uses the configured authenticated middleware stack and delegates login/registration/password/SSO to the host auth layer.   | Shipped | L      | High   | §3                                                                                                                                                                             |