# Contacts — Improvement & Growth Plan

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

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

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.php` — **S**
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.php` — **M**
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.php` — **S**
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.php` — **S**
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.php` — **M**

## 3. Missing Features (gaps)

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

## 4. Issues / Risks

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

## 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

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