Skip to content

Contacts — Improvement & Growth Plan

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

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.

  1. Shipped 2026-06-03: admin search no longer marks encrypted email/display_name columns searchable. ContactResource keeps both fields visible but intentionally avoids SQL LIKE against ciphertext, with regression coverage in ContactsAdminResourceTest. — M
  2. Shipped 2026-06-03: overview stats can be site-scoped. BuildContactsOverviewStatsAction::handle(?int $siteId = null) now applies site_id filters when provided and retains global totals only when explicitly called without a site id, with multi-site regression coverage. — S
  3. Shipped 2026-06-04: source-adapter listeners are queued and failure-isolated. Every first-party listener in src/Listeners implements ShouldQueue, uses Queueable, and delegates source sync through RunsQueuedContactSourceSync, which reports adapter exceptions instead of rethrowing them into the producer workflow. ContactsQueuedListenersTest covers the queue contract and failure isolation. — M
  4. Done/Shipped: Shopify customer sync preloads the connection relation. UpsertShopifyCustomerAction now refreshes and eager-loads connection before dispatching ShopifyCustomerSynced, while SyncShopifyCustomerContactAction only consumes already-loaded relations and no longer runs a fallback relation query per event. — ../shopify-commerce/src/Actions/Customers/UpsertShopifyCustomerAction.php, src/Actions/SyncShopifyCustomerContactAction.phpS
  5. Done/Shipped: source enrichment now prefers newer contact details. FindOrCreateContactAction updates mutable contact fields (email, phone, names, display name) when an incoming source record is newer than the contact’s current last_seen_at, while keeping source_key and source_identifier first-write to preserve canonical source matching. — src/Actions/FindOrCreateContactAction.phpM
  6. Done/Shipped: add site_id + last_seen_at ordering index. The package now registers a guarded contacts_site_last_seen_index migration 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.phpS
  7. Done/Shipped: stats widget aggregates are cached by site. BuildContactsOverviewStatsAction now caches global/site overview counts through ContactsOverviewStatsCache, using the manifest-declared contacts tag 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.phpS
  8. Adapter actions duplicate the same stringValue/intValue/relatedModel helpers — the seven Sync*ContactAction classes (618 lines total) repeat near-identical scalar-coercion and relation-loading boilerplate. Extract a shared AbstractSourceContactAdapter or trait to cut maintenance surface and guarantee consistent null handling. — src/Actions/Sync*ContactAction.phpM

Mapped against declared capabilities[] and CRM norms. Table-stakes unless marked differentiator.

  • Done/Shipped: manual contact merge. Contacts now ships MergeContactsAction plus 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:privacy command. AuditContactPrivacyExportAction and AnonymizeContactWithAuditAction record 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 note activity through RecordContactActivityAction. Lead records now expose a Change status table action backed by UpdateLeadStatusAction, 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. TagContactAction now writes normalized tags to contact_tags and contact_tag_memberships while preserving the existing profile.tags mirror for source-adapter compatibility. Contact::withTag() exposes relation-backed segment queries, and ContactResource adds 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 profile JSON 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. ContactActivity data 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_state are buried in profile, 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'] records last_seen_at per source but overwrites; there is no first-touch attribution or per-source history. (differentiator)
  • Shipped 2026-06-03: health check is real. ContactsHealthCheck now verifies storage tables, morph aliases, and hash-secret readiness, with feature coverage in ContactsHealthCheckTest.
  • Shipped 2026-06-03: capability contacts-deduplication-rules was removed. The manifest test now asserts that the package does not advertise the unimplemented rules/merge engine.
  • hash_secret falls back to app.key. Contact::hashIdentity uses config('capell-contacts.hash_secret') ?: config('app.key') (src/Models/Contact.php:215-220). If APP_KEY is ever rotated, every email_hash/phone_hash/source_identifier_hash becomes 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. BuildContactPrivacyExportAction remains the pure export builder, while AuditContactPrivacyExportAction is 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: 40 and cacheable: false, sensitiveOutput: true. Overview stats now use the declared contacts cache 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-133Low/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*Action return-type/AsAction convention. — tests/Medium
  • i18n: labels are translated (good), but package.php/generic.php ship en only; 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.

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.

ItemBucketEffortImpactSection ref
Fix encrypted-column admin search (drop/replace ->searchable())ShippedSHigh — core admin is currently broken§2.1
Implement real ContactsHealthCheck assertionsShippedSHigh — advertised critical check is fake§4
Site-scope overview stats (close cross-tenant leak)ShippedSHigh — privacy + correctness§2.2, §4
Reconcile contacts-deduplication-rules capability with reality (drop claim or build merge)ShippedSHigh — truth-in-advertising§3, §4
Queue source-adapter listeners + isolate failures (ShouldQueue + try/catch)ShippedMHigh — producer resilience + latency§2.3, §4
Expose privacy export/erasure as admin action + contacts: command, with audit logShippedMHigh — premium/paid GDPR promise§3, §4
Done/Shipped: Add contact view page + manual activity/note + lead status transitions (write surface)DoneLHigh — turns mirror into a CRM§3
Done/Shipped: Contact merge UI + rule config (deliver the dedup capability)DoneLHigh — 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/segmentsDoneMMedium-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 preloadDoneSMedium — bulk-sync N+1§2.4
Revisit first-write-wins fillWhenPresent enrichment policyDoneMMedium — data accuracy§2.5
Shipped: Cache stats widget by site with contacts tag invalidationDoneSMedium — uses declared cacheTags§2.7, §4
Shipped: Add (site_id, last_seen_at) contact ordering indexDoneSMedium — admin list performance§2.6
Add CRM screenshots, README; maintain CHANGELOGShippedSMedium-High — required for certification§5, §4
Extract shared adapter base/trait; add arch + query-budget + search + site-scope testsLaterMMedium — maintainability + regression safety§2.8, §4
CSV import + bulk export; custom-field registry; consent records; source attribution historyLaterLMedium — CRM completeness§3