CRM stores people as WordPress posts so the shared Admin DataViews shell can list them. It does not replace staff bylines, Firebase site members, or Mailchimp audiences.
Domain model
Contact_Fields is the field registry. CPT, REST meta, DataViews columns, bulk edit, and CSV import all read from it.
| Concept | Storage |
|---|---|
| Contact | prc_crm_contact post, public => false. Gutenberg loads because supports includes editor. A locked prc-crm/contact-info block is the canvas UI for contact meta. |
| Display name | post_title, filled from first and last name when the title is empty |
| Email and profile fields | post meta listed in Contact_Fields::LIST_META_FIELDS |
| Segment | non-hierarchical prc_crm_segment |
| Organization | non-hierarchical prc_crm_org |
| Transactional email events | custom table {prefix}prc_crm_email_events |
| Share redirect | prc_crm_share post, public => false |
| Share hits | custom table {prefix}prc_crm_share_hits |
Gender is a closed string set on the registry. Empty is allowed.
Admin list
Contact_List registers the CPT on prc_wp_admin_dataview_register_lists. Rows and gender filters use the shell shape_row / query_args / update_field filters. Segment and organization columns come from prc_wp_admin_dataview_taxonomy_fields. CRM adds those taxonomies. The shell lazy-loads term options and maps filters to tax_query.
The provider script loads only when prc-wp-admin-dataview is enqueued.
Quick add is a HeaderActions button plus a PageExtras DataForm modal. Submit posts to /wp/v2/prc-crm-contacts with source = quick-add. Add New still opens the full editor for segments and organizations. DataForm comes from the shell (window.prcWpAdminDataviewsWp) so the provider does not bundle a second copy of DataViews.
Contact editor
New contacts get a locked prc-crm/contact-info block from the CPT template (template_lock is all). The block edits the same post meta as Quick add. Existing contacts without the block receive it on editor load. Emails and share hits stay in document sidebar panels.
Email Builder
Email_Bridge::compose() writes prc_email_audience_{slug} plus _meta, then inserts a draft prc_email_txn with prc_email_delivery_mode = mandrill. Sending stays in Email Builder. CRM does not call Mandrill.
Each compose creates a new audience snapshot. Re-running compose does not reuse a prior option key.
Compose also writes a queued row per matching contact in {prefix}prc_crm_email_events. When Email Builder marks prc_email_mandrill_send_status as sent or partial, CRM writes sent rows from the audience option. Mandrill open, click, bounce, and reject events arrive at POST /prc-api/v3/crm/mandrill-webhook. CRM matches msg.email to a contact and msg.metadata.email_post_id from the prc_email_mandrill_message filter.
Do not store event lists in post meta. Email_Activity derives contact activity and a Report_Schema summary at read time from the events table.
Activity for one contact on one message is a closed enum. Highest rank wins: queued → sent → not_opened → opened → clicked → bounced | rejected | spam | unsub. Unique opens and clicks are 0 or 1 per contact. Totals are row counts. Click-throughs group by URL.
completed is a boolean beside activity. It is true when a click URL contains crmShare=, or when an identified share hit URL or destination matches a click URL on that message.
summary_for_post() returns a CRM-channel report (opens_unique, rates against sent minus bounce, clicks_by_url with unique and total). CRM replaces the Email Builder engagement envelope through prc_email_builder_report_envelope for Mandrill prc_email_txn posts. Refresh rereads the events table. It does not call Mailchimp.
The contact editor Emails panel reads grouped rows from the events table.
Share redirects
Share_Links registers prc_crm_share under the CRM menu. The public query var is crmShare. A request to /?crmShare=<token> 302s to the destination URL and increments {prefix}prc_crm_share_hits.
Identification, in order:
- Signed
crmContact+crmSigon the personal URL emailquery arg that matches a contact- Logged-in WordPress user whose email matches a contact
- Signed
prc_crm_cidcookie from a prior identified hit
Anonymous hits use contact_id = 0. The contact editor Share links panel lists published shares for the contact's segments (or unscoped shares) plus any share the contact already hit.
The redirect response is uncacheable (DONOTCACHEPAGE + nocache_headers) so repeat visits increment the counter.
CSV import
wp prc crm import upserts on normalized email. It creates organization and segment terms from cell text when those columns are present.
Form action
Form_Create_Contact registers POST /prc-api/v3/form/create-contact. The editor action is createContact on prc-block/form, same registration pattern as Email Builder's sendSystemEmail. CRM owns the class. prc-block-forms stays the form runtime.
Submitted fields map onto Contact_Fields through an alias table in Contact_Upsert. Email is required. Matching emails update the existing contact. New contacts get source = form or form:{formPostId}. CSV import and this action both call Contact_Upsert::save.
Editors can also upsert from Forms → Responses. POST /prc-api/v3/crm/contacts/from-form-responses reads stored response rows (fields plus the denormalized email and name columns) and calls Contact_Upsert::from_form_response(). Spam rows are rejected. The Responses DataViews action is hidden unless CRM is active.