QBit CRM

0.2.0-SNAPSHOT — unreleased, P1 Platform. CRM tables, processes and widgets for QQQ applications. This phase upgrades the build, names, permissions, audit integration and database schema. It does not complete the later 0.2.0 product phases.

Build and verification

Use Java 21 and the included Maven wrapper (Maven 3.9.11). The build uses com.kingsrook:qbit-build-parent:2.1.2 and a default-active qqq-snapshot profile importing qqq-bom-pom:4.1.0-RC.6.

./mvnw -q verify
# Windows PowerShell:
./mvnw.cmd -q verify
# If dependencies are already cached and network resolution is unavailable:
./mvnw.cmd -o -q verify

Verification runs Checkstyle on production and test sources, architecture guardrails (including strict UI API routes), JUnit tests and JaCoCo 0.8.15. Coverage gates are 70% instructions and 90% classes. The current suite runs 372 tests, with no failures or skips, including four H2 integration tests. Tests use H2 2.3.232 and AssertJ 3.27.7. CircleCI uses kingsrook/qqq-orb@0.6.9.

The H2 tests execute the shipped PostgreSQL DDL in PostgreSQL mode. They verify all 33 tables and 395 columns, the 21 declared unique keys, relationship indexes, seven persisted entity types, duplicate list-member rejection and DDL/changelog parity. They do not replace deployment verification against the host's PostgreSQL installation.

Install in a host application

  1. Build/install this unreleased snapshot with ./mvnw install, or resolve the same artifact from your configured snapshot repository. The dependency version is explicit; do not assume the QQQ BOM manages this QBit.
<dependency>
   <groupId>com.kingsrook.qbits</groupId>
   <artifactId>qbit-crm</artifactId>
   <version>0.2.0-SNAPSHOT</version>
</dependency>
  1. Align the host with QQQ 4.1.0-RC.6. For a new database, apply the reference PostgreSQL DDL, or include the packaged Liquibase formatted SQL at db/liquibase/qbit-crm/crm.changelog.sql. Choose one installation path. The Java producer does not create the schema.

  2. Register the host backend and authentication, then produce and add the CRM metadata:

import com.kingsrook.qbits.crm.CrmQBitConfig;
import com.kingsrook.qbits.crm.CrmQBitProducer;

// The host has already registered the backend named "rdbms".
CrmQBitConfig crmConfig = new CrmQBitConfig()
   .withDefaultBackendNameForTables("rdbms");

new CrmQBitProducer()
   .withQBitConfig(crmConfig)
   .produce(qInstance)
   .addSelfToInstance(qInstance);

qInstance.setEnforcePermissionsInCoreActions(true);
  1. Map the host's authenticated roles to the permissions below. Configure the host's core audit storage. Add tables, processes and widgets to the host UI as needed: produceAppSection() currently returns an empty CRM section; P1 does not provide an assembled app or home dashboard.

Configuration and access

  • defaultBackendNameForTables: required backend name.
  • inferBackendNames: defaults to true; sets snake_case SQL column names while Java/QQQ field names remain camelCase. SQL table names equal the QQQ table names. Set false only for an explicit host column mapping.
  • tableMetaDataCustomizer: optional host table customizer.
  • securityFields and recordSecurityLocks: retained in P1, applied to contact, company and deal. Host-managed schema columns must match these fields. This is not yet tenant isolation across every child table or workflow.
Scope Permissions
Main and sub-tables crm.read, crm.write
Settings tables except currency crm.manage.read, crm.manage.write
Currency crm.admin.read, crm.admin.write
Widgets and everyday processes crm.hasAccess
Merge, bulk assignment, list and sequence management processes crm.manage.hasAccess
GDPR anonymization crm.admin.hasAccess

Permissions are independent grants. Every CRM-produced table, process and widget has explicit QPermissionRules with DenyBehavior.HIDDEN and QBit provenance. security.CrmPermissions.ALL lists the grants for host administration and tests. The widget provenance adapter supplies core's SourceQBitAware interface because RC.6's widget class lacks it.

Tables

All 33 table names and table-backed possible-value sources use crm_ snake_case. The 23 enum possible-value sources retain their enum names. Relationship columns have indexes; the reference DDL does not impose foreign-key constraints or host-specific tenant columns. Unique constraints match the current QQQ metadata.

Main tables (6)

crm_contact, crm_company, crm_deal, crm_activity, crm_form_submission, crm_email_list.

Settings tables (15)

crm_pipeline, crm_product, crm_email_template, crm_email_sequence, crm_lead_score_rule, crm_assignment_rule, crm_sales_goal, crm_currency, crm_lifecycle_stage, crm_lead_source, crm_industry, crm_activity_type, crm_activity_outcome, crm_contact_role, crm_win_loss_reason.

Sub-tables (12)

crm_pipeline_stage, crm_consent_record, crm_email_list_member, crm_email_message, crm_sequence_step, crm_sequence_enrollment, crm_deal_contact, crm_deal_product, crm_deal_stage_history, crm_activity_participant, crm_assignment_rule_member, crm_external_mapping.

Contact, company, deal and activity declare core FIELD-level audit rules. The bespoke tag tables, attachment table, audit-log table and bulk tag processes have been removed. 0.2.0 is a fresh-install release with no upgrade path from 0.1.0. Collaboration replacements arrive in a later phase.

Processes

The QBit produces 25 processes with these names and labels. QQQ may additionally generate standard table actions such as <table>.bulkInsert; those retain core's naming conventions. CRM processes are available to host wiring and are not registered as scheduled jobs.

Process name Label
crm_auto_assign Auto Assign
crm_bulk_assign_owner Bulk Assign Owner
crm_calculate_lead_scores Calculate Lead Scores
crm_clone_deal Clone Deal
crm_convert_form_to_contact Convert Form to Contact
crm_convert_lead Convert Lead
crm_detect_duplicates Detect Duplicates
crm_enroll_in_sequence Enroll in Sequence
crm_fire_reminders Fire Reminders
crm_gdpr_anonymize_contact GDPR Anonymize Contact
crm_import_contacts Import Contacts
crm_log_activity Log Activity
crm_manage_contact_on_deal Manage Contact on Deal
crm_merge_companies Merge Companies
crm_merge_contacts Merge Contacts
crm_move_deal_stage Move Deal Stage
crm_process_overdue_tasks Process Overdue Tasks
crm_process_sequence_steps Process Sequence Steps
crm_reopen_deal Reopen Deal
crm_send_email Send Email
crm_subscribe_to_list Subscribe to List
crm_track_email_event Track Email Event
crm_transfer_ownership Transfer Ownership
crm_unenroll_from_sequence Unenroll from Sequence
crm_unsubscribe_from_list Unsubscribe from List

Current behavior and limits

The existing pipeline/stage logic, stage history, product totals, currency conversion, contact/company merge, activity logging, form auto-linking, list subscribe/unsubscribe, scoring and assignment processes remain. Fourteen custom dashboard widget producers remain available for host placement, together with generated child-record widgets.

Some process names describe unfinished behavior inherited from 0.1.0:

  • Send Email and sequence email steps record CRM activity/message state; they do not deliver email. Template merging and live delivery tracking are not implemented here.
  • Sequence execution, lead scoring and overdue-task work require host invocation. No schedules are shipped. Assignment is a process, not an automatic insert hook; reminders do not deliver notifications.
  • Scoring's date-decay operators remain unimplemented. Import Contacts expects pre-parsed input and does not provide the planned streamed bulk-load workflow.
  • GDPR anonymization scrubs the contact, unenrolls sequences and records withdrawn consent. It does not yet erase all linked PII or core audit details. The P1 schema retains existing PII fields; classification, complete erasure and redesigned tenancy are later work.

The later intake APIs, public subscription routes, collaboration integration, real email transport, durable jobs and full app navigation are not part of this phase.

Source layout

CrmQBitProducer discovers entity, process and widget producers. core/ holds contacts, companies, forms, lists and consent; deals/, activities/, email/ and scoring/ hold their respective metadata and behavior. security/ defines permissions, sync/ holds external mappings and currency utilities, and widgets/ contains widget producers/renderers. SQL is shipped under src/main/resources/db/liquibase/qbit-crm/.

Licensed under Apache License 2.0.

Integrating: intake and marketing APIs

Use com.kingsrook.qbits.crm.api from a host with an initialized QContext. The API enforces the current session's CRM permissions and record locks; it does not elevate website callers. The host supplies authentication, tenant scope and abuse protection before invoking intake. First use initializes the six activity codes through normal table actions and needs CRM settings write access.

Configure the signing secret from your secret store and the externally reachable base URL. Never put the secret in source control:

CrmQBitConfig config = new CrmQBitConfig()
   .withDefaultBackendNameForTables("main")
   .withTokenSecret(secretFromHost)
   .withPublicBaseUrl("https://example.com")
   .withMessagingProviderName("email"); // core fallback when qbit-email is absent

CrmApi.isInstalled(instance) checks installation. CrmApi.readiness() returns NOT_CONFIGURED, QBIT_EMAIL or CORE_MESSAGING. qbit-email 0.1.0-RC.3 is an optional dependency, preferred when installed; its own readiness and suppression rules apply. Core fallback uses the named host messaging provider. The core path has no durable send deduplication; qbit-email uses sendOnce with crm:<purpose>:<id>. A refreshed confirmation uses a new token hash in that id.

CrmIntakeApi intake = new CrmIntakeApi();
CrmFormResult lead = intake.submitForm(new CrmFormRequest(
   "contact", FormType.CONTACT, "Ada", "Lovelace", "ada@example.com",
   null, "Example", "Please contact me", false, "Reply to my inquiry",
   "search", "organic", "launch", null, null,
   "https://example.com/contact", null, null, null));

CrmSubscribeResult signup = intake.subscribe(new CrmSubscribeRequest(
   "newsletter", "ada@example.com", "Ada", "Lovelace",
   "Send me the monthly newsletter", "Website",
   "search", "organic", "launch", null, null, null, null));

Create an active crm_email_list with its required unique listKey before subscribing. Double opt-in defaults to true. Pending members receive a signed confirmation link and become eligible only after confirmation. Requests retain the exact consent wording and submission evidence. Contact forms without an explicit opt-in record Reply to inquiry / Legitimate interest, never Email consent. Optional visitor/client identifiers must be precomputed SHA-256 hex hashes; salt the client hash in the host. Raw IPs and user-agent strings have no storage fields. Missing subscriber names use the label "Not provided" because the CRM contact model requires both names.

CrmMarketingApi marketing = new CrmMarketingApi();
List<RecipientScopeOption> scopes = marketing.recipientScopeOptions();
List<CrmRecipient> recipients = marketing.resolveRecipients("crm-list:1");
Eligibility eligibility = marketing.checkEmailEligibility(lead.contactId());
marketing.recordActivity(lead.contactId(), ActivityKind.NOTE, "Qualified lead", "Requested a demo");
String unsubscribeUrl = marketing.buildUnsubscribeUrl(lead.contactId(), null);
marketing.recordUnsubscribe(lead.contactId(), null, "Customer request");

Recipient scopes are crm-list:<id> and crm-contact:<id>. Addresses are returned only when eligible. The latest Email consent must be granted and unexpired; doNotEmail, a global unsubscribe date, or any non-subscribed membership suppresses delivery. List-level withdrawal also appends a contact-level Email withdrawal, so it suppresses marketing until subsequent explicit consent; it does not set the contact's global unsubscribe date. The API never silently clears that global suppression. listFormSubmissions(campaign, since) returns submission and contact IDs plus timestamps, without form contents.

When Javalin is present, CRM registers GET and POST under /qqq/hooks/crm/confirm/{token} and /qqq/hooks/crm/unsubscribe/{token}. GET displays an accessible confirmation button; POST performs the operation. Links expire after 90 days. Confirmation tokens are stored only as SHA-256 hashes; refreshing or withdrawing a pending subscription invalidates its previous token. The signed route uses a system session solely for the IDs authorized by that link. Direct callers can also use confirm(token), unsubscribe(token) and unsubscribeByContact(contactId, listIdOrNull, source). Pages contain no personal data and responses disable caching and referrer forwarding.

S
Description
Customer Relationship Management QBit for the QQQ framework
Readme Apache-2.0
5.9 MiB
0 Stars 3 Watchers 0 Forks
2026-03-29 20:48:45 +00:00
Languages
Java 100%