Contacts — Improvement & Growth Plan
Package: capell-app/contacts · Kind: package · Tier: premium · Product group: Capell Content · Bundle: content-product · Status: Draft
1. Snapshot
Section titled “1. Snapshot”Contacts is an admin-only, schema-owning CRM record layer: five tables (contacts, contact_organisations, contact_organisation_memberships, contact_leads, contact_activities) backing four models (Contact, Organisation, Lead, ContactActivity) and four read-only Filament resources plus a dashboard stats widget. Its real value is the ingest pipeline: SyncContactSourceRecordAction (src/Actions/SyncContactSourceRecordAction.php) calls FindOrCreateContactAction to dedupe by hashed email → phone → source identity, then tags, optionally creates a Lead, and records a ContactActivity. Seven source adapters (Sync*ContactAction) wrap that action, wired to first-party events via Event::listen in ContactsServiceProvider (Form Builder, Access Gate, Events, Comments, Campaign Studio, Shopify ShopifyCustomerSynced); Newsletter calls SyncContactSourceRecordAction directly from SyncNewsletterSubscriberContactAction. PII (email, phone, names, source_identifier, profile, lead context, activity payload) is encrypted at rest with non-reversible HMAC hashes for lookup. Deps: capell-app/core, capell-app/admin, lorisleiva/laravel-actions, spatie/laravel-data. Current marketplace summary now describes the unified CRM layer in buyer-facing terms and no longer overclaims true merge/rule deduplication. Screenshots: 2 promoted Capell runner PNGs for the Contacts table and Contact Activities admin index, with the dashboard stats widget still optional until first-viewport widget targeting is reliable.
2. Improvements (existing functionality)
Section titled “2. Improvements (existing functionality)”- Shipped 2026-06-03: admin search no longer marks encrypted
email/display_namecolumns searchable.ContactResourcekeeps both fields visible but intentionally avoids SQLLIKEagainst ciphertext, with regression coverage inContactsAdminResourceTest. — M - Shipped 2026-06-03: overview stats can be site-scoped.
BuildContactsOverviewStatsAction::handle(?int $siteId = null)now appliessite_idfilters when provided and retains global totals only when explicitly called without a site id, with multi-site regression coverage. — S - Shipped 2026-06-04: source-adapter listeners are queued and failure-isolated. Every first-party listener in
src/ListenersimplementsShouldQueue, usesQueueable, and delegates source sync throughRunsQueuedContactSourceSync, which reports adapter exceptions instead of rethrowing them into the producer workflow.ContactsQueuedListenersTestcovers the queue contract and failure isolation. — M - Done/Shipped: Shopify customer sync preloads the
connectionrelation.UpsertShopifyCustomerActionnow refreshes and eager-loadsconnectionbefore dispatchingShopifyCustomerSynced, whileSyncShopifyCustomerContactActiononly consumes already-loaded relations and no longer runs a fallback relation query per event. —../shopify-commerce/src/Actions/Customers/UpsertShopifyCustomerAction.php,src/Actions/SyncShopifyCustomerContactAction.php— S - Done/Shipped: source enrichment now prefers newer contact details.
FindOrCreateContactActionupdates mutable contact fields (email,phone, names, display name) when an incoming source record is newer than the contact’s currentlast_seen_at, while keepingsource_keyandsource_identifierfirst-write to preserve canonical source matching. —src/Actions/FindOrCreateContactAction.php— M - Done/Shipped: add
site_id+last_seen_atordering index. The package now registers a guardedcontacts_site_last_seen_indexmigration so site-scoped contact lists can use a composite index for default last-seen ordering. —database/migrations/2026_06_06_000001_add_site_last_seen_index_to_contacts_table.php,src/Providers/ContactsServiceProvider.php— S - Done/Shipped: stats widget aggregates are cached by site.
BuildContactsOverviewStatsActionnow caches global/site overview counts throughContactsOverviewStatsCache, using the manifest-declaredcontactstag when available and a plain-cache fallback otherwise. Contact, Organisation, Lead, and ContactActivity model writes invalidate the global key and affected site key. —src/Actions/BuildContactsOverviewStatsAction.php,src/Support/ContactsOverviewStatsCache.php,src/Models/Contact.php,src/Models/Organisation.php,src/Models/Lead.php,src/Models/ContactActivity.php— S - Adapter actions duplicate the same
stringValue/intValue/relatedModelhelpers — the sevenSync*ContactActionclasses (618 lines total) repeat near-identical scalar-coercion and relation-loading boilerplate. Extract a sharedAbstractSourceContactAdapteror trait to cut maintenance surface and guarantee consistent null handling. —src/Actions/Sync*ContactAction.php— M
3. Missing Features (gaps)
Section titled “3. Missing Features (gaps)”Mapped against declared capabilities[] and CRM norms. Table-stakes unless marked differentiator.
- Done/Shipped: manual contact merge. Contacts now ships
MergeContactsActionplus a Contact table merge action. Operators can merge a duplicate into another contact on the same site; the Action moves leads, activities, organisation memberships, and tags, merges profile data, fills missing identity fields on the retained contact, records a non-PII merge activity, and deletes the duplicate. Rule-based automatic deduplication remains future depth. (differentiator) — table-stakes for CRM. - Shipped 2026-06-04: operator-facing privacy workflow. Contact subject-access and erasure workflows are now reachable through Contact admin row actions and the
capell-contacts:privacycommand.AuditContactPrivacyExportActionandAnonymizeContactWithAuditActionrecord non-PII audit activities for exports and erasures. (table-stakes) - Done/Shipped: first CRM write surface. Contacts now has a Contact view page with overview fields and a recent activity timeline, plus an Add note header action that records a
noteactivity throughRecordContactActivityAction. Lead records now expose a Change status table action backed byUpdateLeadStatusAction, stamping qualified/closed timestamps for the relevant transitions. Organisation assignment and broader edit/correction workflows remain later CRM depth. (table-stakes) - Done/Shipped: tags are queryable and filterable.
TagContactActionnow writes normalized tags tocontact_tagsandcontact_tag_membershipswhile preserving the existingprofile.tagsmirror for source-adapter compatibility.Contact::withTag()exposes relation-backed segment queries, andContactResourceadds a translated tag filter for the admin index. Full segment-builder UX remains future CRM depth.src/Models/ContactTag.php,src/Actions/TagContactAction.php,src/Filament/Resources/Contacts/ContactResource.php,database/migrations/2026_06_07_000001_create_contact_tags_tables.php,tests/Feature/ContactFoundationTest.php— M. - Custom fields. Arbitrary
profileJSON exists but there is no schema, no admin-defined custom field registry, no per-field rendering. CRM buyers expect typed custom fields. (table-stakes) - Import / export. No CSV/contact import action and no bulk export beyond the per-contact privacy export. Onboarding a customer with an existing list is impossible today. (table-stakes)
- Activity timeline UI.
ContactActivitydata exists and is recorded, but there is no chronological timeline view on a contact — the activity resource is a flat list. (table-stakes) - Consent / marketing-state as first-class data. Shopify
accepts_marketing/marketing_stateare buried inprofile, not modeled as consent records with timestamps and source — needed for GDPR/lawful-basis tracking and for safe Newsletter/Campaign cross-sell. (differentiator) - Source attribution / first-touch vs last-touch.
profile['sources']recordslast_seen_atper source but overwrites; there is no first-touch attribution or per-source history. (differentiator)
4. Issues / Risks
Section titled “4. Issues / Risks”- Shipped 2026-06-03: health check is real.
ContactsHealthChecknow verifies storage tables, morph aliases, and hash-secret readiness, with feature coverage inContactsHealthCheckTest. - Shipped 2026-06-03: capability
contacts-deduplication-ruleswas removed. The manifest test now asserts that the package does not advertise the unimplemented rules/merge engine. hash_secretfalls back toapp.key.Contact::hashIdentityusesconfig('capell-contacts.hash_secret') ?: config('app.key')(src/Models/Contact.php:215-220). IfAPP_KEYis ever rotated, everyemail_hash/phone_hash/source_identifier_hashbecomes orphaned and dedup silently starts creating duplicates. No migration/rehash path exists. Document the constraint and provide a rehash command, or require an explicit dedicated secret. — Medium- Shipped 2026-06-04: privacy exports are audited.
BuildContactPrivacyExportActionremains the pure export builder, whileAuditContactPrivacyExportActionis used by admin/console workflows to record non-PII export audit activity. - Shipped 2026-06-04: event-consumer resilience. Source adapters now run as queued listeners and report exceptions without rethrowing, so CRM sync failures no longer break the originating form submission, comment, registration, campaign conversion, or Shopify webhook path.
- Cache safety vs declared budget. Manifest sets
adminQueryBudget: 40andcacheable: false, sensitiveOutput: true. Overview stats now use the declaredcontactscache tag where supported, but list page per-row work remains unmeasured and there is no test asserting the query count stays under 40. Add a query-count assertion. —capell.json:122-133— Low/Medium - Test gaps: strong behavioral coverage exists for adapters, identity matching, privacy export/anonymize, admin encrypted-search safety, site-scoped stats, health checks, listener queueing/failure isolation, admin privacy action registration, and console privacy workflows. Missing: (a) no Filament Livewire interaction test (table render/search/sort); (b) no architecture test enforcing
Sync*Actionreturn-type/AsActionconvention. —tests/— Medium - i18n: labels are translated (good), but
package.php/generic.phpshipenonly; no other locales. — Low - Shipped 2026-06-06: package README added. The package root now has buyer-facing setup and operation guidance aligned with
docs/overview.md, privacy workflows, and the current runner-backed screenshot evidence.
5. Marketplace & Selling
Section titled “5. Marketplace & Selling”Critique. The current marketplace summary and composer description are accurate and no longer advertise the removed merge/rules capability. With tier: premium, proposedLicense: paid, requestedCertification: first-party, and privateDocsRequested: true, the package now has a root README plus two promoted Capell runner screenshots for the contact list and activity timeline. The remaining media gap is narrower: show the dashboard widget and organisation/lead resources once the screenshot runner can target those surfaces reliably.
Improved 1-sentence summary:
The unified CRM layer for Capell — automatically captures contacts, organisations, leads, and activity from your forms, newsletter, events, comments, and Shopify store into one privacy-safe, encrypted record.
Improved 3–4 sentence description:
Contacts gives your Capell site a single, GDPR-aware customer record that fills itself. Every form submission, newsletter signup, event registration, comment, campaign conversion, and Shopify customer is matched to one encrypted contact — deduplicated by email, phone, or source — and enriched with a timeline of leads and activity. Admins get read-through CRM resources and an at-a-glance dashboard, while privacy export and anonymization actions support subject-access and erasure requests out of the box. Built as the shared data spine for the Capell content suite, it powers smarter segmentation across Newsletter, Campaign Studio, and Form Builder. (Note: tighten “deduplicated” claim to match shipped behavior until merge/rules land — see §3/§4.)
Media gaps. The contacts list and activity timeline/index screenshots are now promoted from committed Capell runner PNGs. Add at least: (1) dashboard stats widget once widget targeting is reliable, (2) organisation resource, (3) lead resource/status view, and (4) a clearer privacy export/erasure action state if certification asks for explicit GDPR workflow media.
Pricing / tier / bundle. Premium in content-product is defensible only once it has a write surface and at least the privacy workflow + merge are reachable; as a read-only mirror it is closer to a free/infra dependency. Position it as the hub of the suite: it supports access-gate, campaign-studio, comments, events, form-builder, newsletter, shopify-commerce. Cross-sell levers: bundle Contacts as the prerequisite that unlocks segmentation value in Newsletter and Campaign Studio (both already integrate), surface “contacts captured from this form” in Form Builder, and “customers synced” in Shopify Commerce. Target buyer: site owners/marketers running multiple capture channels who want one customer view without a third-party CRM.
Keywords/tags (8–12): crm, contacts, leads, organisations, customer-data, gdpr, data-privacy, segmentation, shopify-contacts, newsletter-crm, lead-capture, activity-timeline.
6. Prioritized Roadmap
Section titled “6. Prioritized Roadmap”| Item | Bucket | Effort | Impact | Section ref |
|---|---|---|---|---|
Fix encrypted-column admin search (drop/replace ->searchable()) | Shipped | S | High — core admin is currently broken | §2.1 |
Implement real ContactsHealthCheck assertions | Shipped | S | High — advertised critical check is fake | §4 |
| Site-scope overview stats (close cross-tenant leak) | Shipped | S | High — privacy + correctness | §2.2, §4 |
Reconcile contacts-deduplication-rules capability with reality (drop claim or build merge) | Shipped | S | High — truth-in-advertising | §3, §4 |
Queue source-adapter listeners + isolate failures (ShouldQueue + try/catch) | Shipped | M | High — producer resilience + latency | §2.3, §4 |
Expose privacy export/erasure as admin action + contacts: command, with audit log | Shipped | M | High — premium/paid GDPR promise | §3, §4 |
| Done/Shipped: Add contact view page + manual activity/note + lead status transitions (write surface) | Done | L | High — turns mirror into a CRM | §3 |
| Done/Shipped: Contact merge UI + rule config (deliver the dedup capability) | Done | L | High — biggest differentiator | §3, §4 — manual merge Action and admin row action shipped; automatic rule configuration remains future CRM depth. |
Done/Shipped: Move tags out of encrypted profile into queryable relation + filtering/segments | Done | M | Medium-High — unlocks cross-sell | §3 — tag tables, relation-backed sync, withTag() scope, and admin tag filter shipped; richer segment-builder UX remains future depth. |
Eager-load Shopify connection; require emitters to preload | Done | S | Medium — bulk-sync N+1 | §2.4 |
Revisit first-write-wins fillWhenPresent enrichment policy | Done | M | Medium — data accuracy | §2.5 |
Shipped: Cache stats widget by site with contacts tag invalidation | Done | S | Medium — uses declared cacheTags | §2.7, §4 |
Shipped: Add (site_id, last_seen_at) contact ordering index | Done | S | Medium — admin list performance | §2.6 |
| Add CRM screenshots, README; maintain CHANGELOG | Shipped | S | Medium-High — required for certification | §5, §4 |
| Extract shared adapter base/trait; add arch + query-budget + search + site-scope tests | Later | M | Medium — maintainability + regression safety | §2.8, §4 |
| CSV import + bulk export; custom-field registry; consent records; source attribution history | Later | L | Medium — CRM completeness | §3 |