Skip to content

Customer Portal — Improvement & Growth Plan

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

Customer Portal is the authenticated self-service foundation for Capell sites. It exposes two surfaces: a frontend dashboard (3 routes under /portal behind web,authShowCustomerPortalController, 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.

  • 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.phpM
  • Validate support requests in one placeStorePortalSupportRequestController 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.phpS
  • 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.phpS
  • Reuse the priority/status option helpersShowCustomerPortalController::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.phpS

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.
  • 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.

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.)

ItemBucketEffortImpactSection ref
Scope PortalSupportRequestResource per site (getEloquentQuery)ShippedSHigh§2, §4
Add tenant-isolation test (account A cannot see B’s data)ShippedSHigh§4
Implement real health-check probe (tables + models)ShippedSHigh§2, §4
Resolve portal-profile: add resolve Action + render, or remove dead registriesShippedMHigh§3, §4
Add factories (+ optional seeder) for both modelsShippedSMed§3
Add unauthenticated/throttle/suspended negative-path testsShippedSMed§4
Enforce PortalAccountStatus (block suspended/archived)ShippedSMed§3
Emit events + notifications on submit / status changeShippedMHigh§3
Set real frontendRenderBudgetMs + frontend query budgetShippedSMed§4
Done/Shipped: Capture marketplace screenshots (4 surfaces) + rewrite summary/descriptionShippedSHigh§5
Schema-drive preferences (single source for view + validation)ShippedMMed§2, §3
Done/Shipped: Cap / paginate dashboard + self-service provider fan-outShippedMMed§2
Add README + CHANGELOGShippedSMed§4
Two-way support threading (replies + attachments)ShippedLHigh§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.ShippedLHigh§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.ShippedLHigh§3