Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
520be36c5d |
No files matched your search
@@ -1,14 +1,14 @@
|
||||
version: 2.1
|
||||
setup: true
|
||||
orbs:
|
||||
qqq-orb: kingsrook/qqq-orb@0.7.12
|
||||
qqq-orb: kingsrook/qqq-orb@0.7.15
|
||||
workflows:
|
||||
setup:
|
||||
jobs:
|
||||
- qqq-orb/generate_pipeline:
|
||||
orb_version: 0.7.12
|
||||
orb_version: 0.7.15
|
||||
filters:
|
||||
branches:
|
||||
ignore: automation/qbit-rc-pending
|
||||
tags:
|
||||
only: /^v\d+\.\d+\.\d+$/
|
||||
only: /v.*/
|
||||
@@ -1,2 +0,0 @@
|
||||
mvnw text eol=lf
|
||||
mvnw.cmd text eol=crlf
|
||||
@@ -1 +0,0 @@
|
||||
* @KofTwentyTwo
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
wrapperVersion=3.3.4
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
|
||||
+5
-5
@@ -1,16 +1,16 @@
|
||||
profile: qbit
|
||||
orb_version: 0.7.12
|
||||
orb_version: 0.7.15
|
||||
default_branch: develop
|
||||
java_version: '21'
|
||||
browsers: true
|
||||
api_checks: false
|
||||
docs_only: false
|
||||
release_on_main: false
|
||||
release_tag_policy: stable
|
||||
test_branch_policy: non-release
|
||||
release_on_main: true
|
||||
release_tag_policy: v-prefix
|
||||
test_branch_policy: feature-no-hotfix
|
||||
publish:
|
||||
snapshot: true
|
||||
release_candidate: true
|
||||
release: true
|
||||
hotfix: true
|
||||
asciidoc: false
|
||||
release_completion: true
|
||||
@@ -1,15 +0,0 @@
|
||||
# QBit CRM
|
||||
|
||||
CRM metadata, processes and widgets for QQQ. P1 upgrades the platform; later phases are defined in the 0.2.0 design.
|
||||
|
||||
## Build
|
||||
|
||||
Use Java 21 and the Maven wrapper: `./mvnw -q verify` runs checkstyle, tests and JaCoCo (70% instructions, 90% classes). Parent: `qbit-build-parent:2.1.2`; default-active `qqq-snapshot` BOM: `4.1.0-RC.6`. Architecture guardrails and strict UI API routes stay enabled.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Entry point: `CrmQBitProducer`, with package-discovered metadata producers.
|
||||
- Table, join, widget and process names use `crm_` snake_case. Database columns use snake_case; Java fields stay camelCase.
|
||||
- Allman braces, 3-space indentation, `return (x);`, fluent setters and Javadoc flower-box banners.
|
||||
- New source files carry the Apache 2.0 license header.
|
||||
- Keep changes scoped and run verify before committing. Do not push without authorization.
|
||||
@@ -1,21 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased / 0.2.0
|
||||
|
||||
- Read the CRM QBit metadata version from the Maven-filtered artifact version so release metadata matches the build.
|
||||
|
||||
- Adopt QQQ 4.1.0-RC.7 native help bundles, replacing the RC.6 stand-in loader with host-neutral CRM help, live references and the core coverage gate.
|
||||
|
||||
- Upgrade to Java 21, QQQ 4.1.0-RC.6 and qbit-build-parent 2.1.2.
|
||||
- Add Maven wrapper, CI configuration, architecture guardrails and coverage gates.
|
||||
- Rename tables, joins, widgets and CRM processes to `crm_` snake_case, with inferred snake_case SQL columns.
|
||||
- Remove bespoke tag, attachment and audit tables, bulk tag processes and unused namespace/prefix configuration.
|
||||
- Use core FIELD-level auditing on contacts, companies, deals and activities.
|
||||
- Apply CRM, management and admin permission rules and source-QBit metadata, including an RC.6 widget provenance adapter.
|
||||
- Ship PostgreSQL DDL, 33 Liquibase changesets, and H2 persistence and schema tests. 0.2.0 is a fresh install; there is no upgrade path from 0.1.0.
|
||||
- Correct installation instructions and document the existing P1 behavior and remaining limitations.
|
||||
- Add `crm-defaults.changelog.sql`: insert-only starter data (Sales Pipeline and stages, lifecycle stages, lead sources, industries, win/loss reasons, contact roles, activity types and outcomes, USD base currency). See UPGRADE.md.
|
||||
- Add a Setup card to the CRM home (default pipeline, stages, lifecycle stages, base currency, email delivery).
|
||||
- Every table help now says how its rows get created; `crm_external_mapping` is documented as not written by the CRM yet.
|
||||
- Stamp `crm_currency.rateLastUpdatedDate` when a rate is set or changed; the field is now read-only.
|
||||
- Add `CrmTablesGuardrailTest` and a Liquibase-driven defaults test (liquibase-core as a test dependency).
|
||||
@@ -1,15 +0,0 @@
|
||||
# qbit-crm
|
||||
|
||||
## Knowledge base
|
||||
|
||||
Reviewed dossiers for this repo and the QQQ platform live in the second-brain vault:
|
||||
|
||||
- Hub / start here: `R:/Git.Local/KofTwentyTwo/second-brain/knowledge/qqq/qqq-hub.md`
|
||||
- This repo's dossier: `R:/Git.Local/KofTwentyTwo/second-brain/knowledge/qqq/repos/qbit-crm.md`
|
||||
(reviewed commit `76b95c91ee7e`, branch `develop`, 2026-07-04)
|
||||
- QBit mechanics refresher: `knowledge/qqq/architecture/metadata-model.md` in the same vault
|
||||
|
||||
Key facts captured there: parent pom `qbit-build-parent:1.4.0` pins qqq to 0.27.9 (via
|
||||
`qqq-bom-pom` import); two BREAK-04 compile breaks block a qqq 4.0 upgrade
|
||||
(`BaseTest.setAuthentication`, `EnrollInSequenceProcess input.getSession()`); Maven
|
||||
Central's published `0.1.0` predates the AGPL→Apache license switch and the audit fixes.
|
||||
@@ -1,278 +1,485 @@
|
||||
# QBit CRM
|
||||
# QBit: CRM
|
||||
|
||||
**0.2.0-SNAPSHOT — unreleased, P1 Platform + P2 App, home and nav.** CRM tables, processes and widgets for QQQ applications. This work upgrades the build, names, permissions, audit integration and database schema, and supplies the CRM app and record navigation. It does not complete the later 0.2.0 product phases.
|
||||
[](https://github.com/QRun-IO/qbit-crm)
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
[](https://adoptium.net/)
|
||||
|
||||
## Build and verification
|
||||
> **Customer Relationship Management for QQQ Applications - Activity-Centric, Pipeline-Driven**
|
||||
|
||||
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`.
|
||||
This QBit provides a complete CRM system for QQQ applications, covering contact and company management, deal pipeline tracking, activity logging, email tracking, product catalog, lead scoring, assignment automation, website form capture, mailing list subscriptions, and reporting.
|
||||
|
||||
```sh
|
||||
./mvnw -q verify
|
||||
# Windows PowerShell:
|
||||
./mvnw.cmd -q verify
|
||||
# If dependencies are already cached and network resolution is unavailable:
|
||||
./mvnw.cmd -o -q verify
|
||||
## Core Capabilities
|
||||
|
||||
- **Contact & Company Management**: Full lifecycle tracking with tagging, segmentation, and duplicate detection
|
||||
- **Deal Pipeline**: Configurable multi-pipeline stages with probability weighting, rot/aging detection, and stage-gating rules
|
||||
- **Activity Tracking**: Unified activity table for calls, emails, meetings, notes, and tasks with per-record timelines
|
||||
- **Email Integration**: Tracked email sending, open/click/bounce detection, templates with merge fields
|
||||
- **Email Sequences**: Multi-step automated outreach with configurable send windows and failure handling
|
||||
- **Lead Scoring**: Rule-based scoring with dot-notation field paths and time-decay operators
|
||||
- **Assignment Automation**: Round-robin, least-active, and rule-based auto-assignment
|
||||
- **Website Forms**: Capture web form submissions with UTM tracking and lead conversion
|
||||
- **Mailing Lists**: Newsletter and subscription management independent of sales sequences
|
||||
- **GDPR Compliance**: Consent tracking, right-to-erasure via anonymization (not deletion)
|
||||
- **Multi-Currency**: Exchange rate table with converted amounts for accurate pipeline reporting
|
||||
- **Immutable Audit Log**: Field-level change tracking with anonymization support for GDPR
|
||||
- **14 Dashboard Widgets**: Pipeline summary, forecast, leaderboard, conversion rates, win rate, activity feed, and more
|
||||
|
||||
## Open Source & Full Control
|
||||
|
||||
QBit CRM is 100% open source under Apache 2.0. All data stays in your systems. No external CRM services required.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Design Principles
|
||||
|
||||
1. **Activity-centric**: A unified `crm_activity` table is the heart of the system. Every interaction (calls, emails, meetings, notes, tasks) is an activity linked to contacts, companies, and deals. One table to query, one timeline per record.
|
||||
|
||||
2. **Pipeline-driven**: Deals move through configurable pipeline stages. Each stage has probability weighting, rot thresholds, and optional required-field gating. Stage transitions are first-class events captured in `crm_deal_stage_history` for reliable analytics.
|
||||
|
||||
3. **Perpetual activity log**: The `crm_audit_log` table is immutable and append-only. Every meaningful change is recorded with field-level old/new values. For GDPR, PII is anonymized in-place rather than deleted.
|
||||
|
||||
### Technology Stack
|
||||
|
||||
- **Java 17+** with QQQ backend modules
|
||||
- **QQQ Framework**: Entities, processes, widgets, permissions, API layer
|
||||
- **Database**: RDBMS through QQQ's backend abstraction
|
||||
- **MemoryRecordStore**: Full test suite runs in-memory (no database required)
|
||||
|
||||
### Module Organization
|
||||
|
||||
```
|
||||
qbit-crm/
|
||||
src/main/java/com/kingsrook/qbits/crm/
|
||||
CrmQBitConfig.java -- Configuration (backend, security, companion QBits)
|
||||
CrmQBitProducer.java -- Gen 2 QBitMetaDataProducer (auto-discovers all entities)
|
||||
core/ -- Contacts, Companies, Tags, Attachments, Forms, Email Lists
|
||||
deals/ -- Deals, Pipelines, Stages, Products, Line Items, Stage History
|
||||
activities/ -- Activities, Activity Types, Outcomes, Participants
|
||||
email/ -- Email Messages, Templates, Sequences, Enrollments
|
||||
scoring/ -- Lead Score Rules, Assignment Rules, Sales Goals
|
||||
audit/ -- Immutable audit log + CrmAuditLogCustomizer
|
||||
sync/ -- External system mapping, Currency management
|
||||
widgets/ -- 14 dashboard widgets (producers + renderers)
|
||||
```
|
||||
|
||||
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`.
|
||||
## Getting Started
|
||||
|
||||
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.
|
||||
### Prerequisites
|
||||
|
||||
## Install in a host application
|
||||
- **Java 17+**
|
||||
- **Maven 3.8+**
|
||||
- **A QQQ Application** with an RDBMS backend (PostgreSQL, MySQL, H2, etc.)
|
||||
- **QQQ Maven Registry** access (for resolving the dependency)
|
||||
|
||||
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.
|
||||
### Step 1: Add the Maven dependency
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
<artifactId>qbit-crm</artifactId>
|
||||
<version>0.2.0-SNAPSHOT</version>
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
<artifactId>qbit-crm</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
2. Align the host with QQQ **4.1.0-RC.6**. For a new database, apply [the reference PostgreSQL DDL](docs/ddl/postgres.sql), 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.
|
||||
If you are using the QQQ BOM POM, the version is managed for you:
|
||||
```xml
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-bom-pom</artifactId>
|
||||
<version>${qqq.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
```
|
||||
|
||||
3. Register the host backend and authentication, then produce and add the CRM metadata:
|
||||
### Step 2: Configure and produce the QBit
|
||||
|
||||
In your application's metadata setup class (where you build your `QInstance`), add the CRM QBit. The `defaultBackendNameForTables` must match a backend you have already registered with your QInstance (typically your RDBMS backend).
|
||||
|
||||
```java
|
||||
import com.kingsrook.qbits.crm.CrmQBitConfig;
|
||||
import com.kingsrook.qbits.crm.CrmQBitProducer;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
|
||||
|
||||
// In your QInstance setup method:
|
||||
public void defineQInstance(QInstance qInstance)
|
||||
{
|
||||
// ... your existing backend, auth, and app setup ...
|
||||
|
||||
///////////////////////////////
|
||||
// configure and produce CRM //
|
||||
///////////////////////////////
|
||||
CrmQBitConfig crmConfig = new CrmQBitConfig()
|
||||
.withDefaultBackendNameForTables("rdbms"); // must match a registered backend name
|
||||
|
||||
CrmQBitProducer crmProducer = new CrmQBitProducer()
|
||||
.withQBitConfig(crmConfig);
|
||||
|
||||
MetaDataProducerMultiOutput crmOutput = crmProducer.produce(qInstance);
|
||||
crmOutput.addSelfToInstance(qInstance);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Create the database tables
|
||||
|
||||
The QBit defines 39 tables. On first run against a new database, you need to create the schema. QQQ can auto-create tables if your backend supports it, or you can generate DDL from the entity metadata.
|
||||
|
||||
For **development/testing** with H2 or auto-DDL:
|
||||
```java
|
||||
// QQQ will auto-create tables in memory backends.
|
||||
// For RDBMS with auto-DDL enabled, tables are created on startup.
|
||||
```
|
||||
|
||||
For **production** with PostgreSQL/MySQL, generate migration scripts from the entity field definitions. Each entity class documents its table name (e.g., `crmContact`, `crmCompany`, `crmDeal`) and all field types. Column names in the database are `snake_case` versions of the Java `camelCase` field names (e.g., `firstName` -> `first_name`).
|
||||
|
||||
### Step 4: Add CRM to your app navigation
|
||||
|
||||
```java
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||
|
||||
QAppMetaData app = new QAppMetaData()
|
||||
.withName("myApp")
|
||||
.withLabel("My Application")
|
||||
.withSections(List.of(
|
||||
CrmQBitProducer.produceAppSection(),
|
||||
// ... your other app sections ...
|
||||
));
|
||||
qInstance.addApp(app);
|
||||
```
|
||||
|
||||
This adds a "CRM" navigation section to the QQQ Material Dashboard with links to all CRM tables.
|
||||
|
||||
### Step 5 (optional): Seed reference data
|
||||
|
||||
The CRM works without seed data, but you'll want to populate the configuration tables before users start working:
|
||||
|
||||
- **`crmLifecycleStage`**: Lead, Marketing Qualified, Sales Qualified, Opportunity, Customer, Evangelist
|
||||
- **`crmLeadSource`**: Website, Referral, Cold Call, Trade Show, Social Media, Advertisement, Partner
|
||||
- **`crmIndustry`**: Technology, Healthcare, Finance, Manufacturing, Retail, etc.
|
||||
- **`crmActivityType`**: Call, Email, Meeting, Note, Task (with `isSystem=true`)
|
||||
- **`crmActivityOutcome`**: Connected, Left Voicemail, No Answer (for Call type); Completed, No Show, Rescheduled (for Meeting type)
|
||||
- **`crmPipeline`** + **`crmPipelineStage`**: At minimum one pipeline with stages (Prospecting, Qualification, Proposal, Negotiation, Closed Won, Closed Lost)
|
||||
- **`crmContactRole`**: Decision Maker, Influencer, Champion, Budget Holder, End User, Evaluator
|
||||
- **`crmWinLossReason`**: Better Fit (WIN), Price (WIN), Lost to Competitor (LOSS), No Budget (LOSS), etc.
|
||||
- **`crmCurrency`**: At minimum USD with `isBaseCurrency=true` and `exchangeRateToBase=1.0`
|
||||
|
||||
You can insert these via QQQ's `InsertAction` in an application startup process, or directly via SQL.
|
||||
|
||||
### Step 6 (optional): Multi-tenant security
|
||||
|
||||
For SaaS or multi-tenant deployments where each client's CRM data must be isolated:
|
||||
|
||||
```java
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
|
||||
// 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);
|
||||
.withDefaultBackendNameForTables("rdbms")
|
||||
.withRecordSecurityLocks(List.of(
|
||||
new RecordSecurityLock()
|
||||
.withFieldName("clientId")
|
||||
.withSecurityKeyType("crmClientAccess")
|
||||
.withNullValueBehavior(NullValueBehavior.DENY)
|
||||
));
|
||||
```
|
||||
|
||||
4. Map the host's authenticated roles to the permissions below. Configure the host's core audit storage. The `crm` app is enabled by default, with main and Settings navigation and a real-data home. Set `withAppEnabled(false)` to embed the tables in the host's own app. `produceAppSection()` returns the same populated main section; processes are record/table actions, never navigation entries.
|
||||
This injects a `clientId` security field onto all root CRM tables (Contact, Company, Deal, Pipeline) and propagates security locks through join chains to child tables. Users only see records matching their security key values.
|
||||
|
||||
## Configuration and access
|
||||
### Step 7 (optional): Companion QBits
|
||||
|
||||
- `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.
|
||||
- `withTenantSecurityKeyType("tenant")`: enables nullable `tenantKey` fields and record locks. It defaults to null (off). `withTenantAllAccessKeyName("allTenants")` names the host's all-access key. Existing host key types are preserved.
|
||||
- Tenant-enabled hosts separately include `src/main/resources/db/liquibase/qbit-crm/crm-tenancy.changelog.sql` **after** the base changelog and explicitly select the `crm-tenancy` context. Single-tenant hosts omit that file. Child records use parent join locks; polymorphic external mappings and internal job checkpoints carry direct tenant keys. Hosts must supply tenant keys on new direct-key records.
|
||||
- `withApiName(...)` and `withApiVersion(...)`: publish main and settings tables into an existing host API definition. The host supplies `ApiInstanceMetaData` and includes optional `qqq-middleware-api` at runtime. Main tables declare core search fields.
|
||||
- `withEsbDestinationName(...)`: reserved but rejected on RC.6. Its ESB event factory copies all values and has no classified-field exclusion; CRM does not publish those payloads.
|
||||
- `CrmBulkLoadProfiles.ensure()`: call once in host startup after registering core's `SavedBulkLoadProfileMetaDataProvider`. It seeds three CSV mappings by label without replacing edited profiles. CRM does not register the core profile tables.
|
||||
- `CrmHealthChecks.indicators(monitoringSession)`: register the returned indicators with the host health middleware. Include optional `qqq-middleware-health`; the supplied session determines tenant scope and is restored on worker threads. `run()` uses the caller's context. No check adds permissions or tenant keys.
|
||||
|
||||
The shipped help bundle and health adapter are temporary core integration adapters; their removal target is RC.9. See [P4B implementation notes](docs/P4B-IMPLEMENTATION.md) for RC.6 limits.
|
||||
|
||||
| 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 app and widget provenance adapters supply core's `SourceQBitAware` interface because RC.6's app and widget classes lack it.
|
||||
|
||||
## Starter defaults
|
||||
|
||||
Include `db/liquibase/qbit-crm/crm-defaults.changelog.sql` in the host changelog after `crm.changelog.sql` and, on a tenant-enabled install, after `crm-tenancy.changelog.sql` as well:
|
||||
|
||||
```yaml
|
||||
- include: { file: db/liquibase/qbit-crm/crm-defaults.changelog.sql }
|
||||
```
|
||||
|
||||
It seeds, insert-only: the activity types and call/meeting outcomes; the default `Sales Pipeline` (Qualification 10%, Discovery 25%, Proposal 50%, Negotiation 75%, Closed Won 100%, Closed Lost 0%); lifecycle stages (the code looks up `Lead` and `Opportunity` by name); lead sources (`Website` is the web-form default); industries; win and loss reasons (prefixed `Won:` / `Lost:` because names are unique); contact roles; and a USD base currency. Lists are seeded only while empty, so a configured host is unchanged. The lifecycle stages (they change what intake and Convert lead assign) and the base currency are seeded only on a fresh install (no contact, respectively no deal, yet), and the stages only when `Sales Pipeline` is the only pipeline. Rollbacks are empty, so they never delete host rows. Tenant-enabled hosts seed per tenant (the changelog skips tenant-keyed tables). The Setup card on the CRM home shows what is still missing.
|
||||
|
||||
## Tables
|
||||
|
||||
All 34 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 28 table-bound record actions. Every action uses the standard
|
||||
streamed ETL Review and Result screens with linked summary lines. Required inputs
|
||||
are collected before Review. Processes never appear in app navigation. Find
|
||||
Duplicates scans the selected records (select all matching records to scan the
|
||||
table); each duplicate group links to its members. Merges require exactly two
|
||||
selected records and a choice of which record to keep.
|
||||
|
||||
| Table | Process name | Label |
|
||||
|---|---|---|
|
||||
| `crm_contact` | `crm_bulk_assign_owner` | Assign Owner |
|
||||
| `crm_contact` | `crm_auto_assign` | Auto-Assign |
|
||||
| `crm_contact` | `crm_subscribe_to_list` | Add to List |
|
||||
| `crm_contact` | `crm_unsubscribe_from_list` | Remove from List |
|
||||
| `crm_contact` | `crm_enroll_in_sequence` | Enroll in Sequence |
|
||||
| `crm_contact` | `crm_unenroll_from_sequence` | Unenroll from Sequence |
|
||||
| `crm_contact` | `crm_log_activity` | Log Activity |
|
||||
| `crm_contact` | `crm_recalculate_score` | Recalculate Score |
|
||||
| `crm_contact` | `crm_merge_contacts` | Merge Contacts |
|
||||
| `crm_contact` | `crm_detect_duplicates` | Find Duplicates |
|
||||
| `crm_contact` | `crm_convert_lead` | Create Deal |
|
||||
| `crm_contact` | `crm_gdpr_anonymize_contact` | Erase Personal Data |
|
||||
| `crm_contact` | `crm_send_email` | Send Email |
|
||||
| `crm_company` | `crm_assign_company_owner` | Assign Owner |
|
||||
| `crm_company` | `crm_merge_companies` | Merge Companies |
|
||||
| `crm_company` | `crm_find_duplicate_companies` | Find Duplicates |
|
||||
| `crm_company` | `crm_log_company_activity` | Log Activity |
|
||||
| `crm_deal` | `crm_move_deal_stage` | Move Stage |
|
||||
| `crm_deal` | `crm_mark_deal_won` | Mark Won |
|
||||
| `crm_deal` | `crm_mark_deal_lost` | Mark Lost |
|
||||
| `crm_deal` | `crm_reopen_deal` | Reopen |
|
||||
| `crm_deal` | `crm_clone_deal` | Clone |
|
||||
| `crm_deal` | `crm_transfer_ownership` | Assign Owner |
|
||||
| `crm_deal` | `crm_manage_contact_on_deal` | Add Contact |
|
||||
| `crm_deal` | `crm_log_deal_activity` | Log Activity |
|
||||
| `crm_form_submission` | `crm_convert_form_to_contact` | Convert to Contact |
|
||||
| `crm_email_sequence` | `crm_activate_sequence` | Activate |
|
||||
| `crm_email_sequence` | `crm_pause_sequence` | Pause |
|
||||
|
||||
Move Stage and Reopen choose a stage from the selected deals' pipeline. Select
|
||||
one pipeline at a time for those actions. Mark Won and Mark Lost require a
|
||||
win/loss reason and resolve the terminal stage within each deal's pipeline.
|
||||
|
||||
Two hidden background processes have no table binding: `crm_process_sequence_steps`
|
||||
and `crm_calculate_lead_scores`. Both use id keyset pages of 200. They have no
|
||||
shipped schedules yet; the host can invoke them. QQQ also generates standard
|
||||
bulk-load and bulk-edit actions such as `<table>.bulkInsert`.
|
||||
|
||||
Fire Reminders and Process Overdue Tasks were removed in favor of collaboration
|
||||
tasks and alerts. Import Contacts was removed in favor of core bulk load. Track
|
||||
Email Event was removed in favor of the later delivery sync.
|
||||
|
||||
## 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. The CRM home contains Today (five standard statistics), Pipeline by stage, Forecast (current month plus five), Needs attention (ten oldest rotting deals), Recent web leads (latest ten), Goals (up to fifty current goals, hidden when none are visible), and Leaderboard (quarter-to-date won revenue by owner). Reporting boundaries use UTC. Monetary summaries use base currency, with weighted amounts grouped by currency before conversion. Aggregate rows link to their underlying records; stage name and deal count share a column to keep tables to three columns. The pipeline picker appears when multiple active pipelines are visible.
|
||||
|
||||
The contact Timeline shows the latest fifty recorded events from activities, email messages, form submissions and consent, newest first by creation date. Native child lists connect all specified parent screens. Deal primary-contact and email-message contact references are included in the fresh-install schema. The trend widgets, redundant recent-deals/activity-count widgets and activity feed have been retired.
|
||||
|
||||
Later phases complete the following behavior:
|
||||
|
||||
- Send Email is a non-sending action shell. It checks for `CrmEmailTransport` with `Class.forName` and reports **Email sending arrives with the email transport** when absent. It never writes a fake sent message or activity. If the class is present, the shell reports that action integration is still pending. Sequence email steps retain the existing activity-only behavior until transport integration.
|
||||
- Sequence execution and lead scoring require host invocation. No schedules are shipped. Assignment remains an action; automatic insert hooks, collaboration and alert integration arrive later.
|
||||
- Activate/Pause update the sequence active flag. The existing sequence runner's business logic is preserved in P2B and does not yet consult that flag; enforcing it during execution is a follow-up.
|
||||
- Scoring's date-decay operators remain unimplemented. Core bulk load is the import path; saved CRM import profiles arrive later.
|
||||
- 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 the remaining P2 security/integration work are not part of this phase.
|
||||
|
||||
## In-app help
|
||||
|
||||
CRM registers [help/qbit-crm.yaml](src/main/resources/help/qbit-crm.yaml) from
|
||||
`CrmQBitProducer`. The bundle uses the `QBIT` layer and the current artifact version.
|
||||
Registration only adds metadata and works without a `helpContent` table; QQQ's
|
||||
startup sync skips hosts without that table. Hosts with help storage load help
|
||||
and call `HelpBundles.syncOnStartup(qInstance)` after defining their metadata.
|
||||
|
||||
To customize wording or describe your deployment, register a separate `HOST`
|
||||
bundle with the same entry keys (and roles). HOST text takes precedence over QBIT
|
||||
text; the sync preserves user edits for review. Keep live `{{...}}` references
|
||||
when referring to labels, defaults or enum values.
|
||||
|
||||
`CrmPublicationHelpTest` runs `HelpBundleAssertions` against the produced and
|
||||
validated CRM instance, including generated record actions, and also requires
|
||||
widget and possible-value help. `CrmTablesGuardrailTest` checks every table's
|
||||
row-creation guidance. Run both through `./mvnw -q verify`.
|
||||
|
||||
## 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. `actions/` defines the streamed record actions and their inputs/summaries. `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](LICENSE).
|
||||
|
||||
## 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:
|
||||
The CRM integrates with other QBits for enhanced functionality:
|
||||
|
||||
```java
|
||||
CrmQBitConfig config = new CrmQBitConfig()
|
||||
.withDefaultBackendNameForTables("main")
|
||||
.withTokenSecret(secretFromHost)
|
||||
.withPublicBaseUrl("https://example.com")
|
||||
.withMessagingProviderName("email"); // core fallback when qbit-email is absent
|
||||
CrmQBitConfig crmConfig = new CrmQBitConfig()
|
||||
.withDefaultBackendNameForTables("rdbms")
|
||||
.withQuickSearchQBitNamespace("quickSearch") // full-text search across contacts/companies/deals
|
||||
.withWebhooksQBitNamespace("webhooks") // fire events on CRM changes (deal won, contact created, etc.)
|
||||
.withWorkflowsQBitNamespace("workflows"); // user-defined automation rules
|
||||
```
|
||||
|
||||
`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.
|
||||
Each companion QBit must be separately added as a Maven dependency and produced into your QInstance. The namespace strings must match the namespaces you used when producing those QBits.
|
||||
|
||||
### Complete example
|
||||
|
||||
```java
|
||||
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));
|
||||
public void defineQInstance(QInstance qInstance)
|
||||
{
|
||||
///////////////
|
||||
// backend //
|
||||
///////////////
|
||||
qInstance.addBackend(new RDBMSBackendMetaData()
|
||||
.withName("rdbms")
|
||||
.withVendor("postgresql")
|
||||
.withHostName("localhost")
|
||||
.withDatabaseName("myapp")
|
||||
.withUsername("myapp")
|
||||
.withPassword("secret"));
|
||||
|
||||
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));
|
||||
////////////////////
|
||||
// authentication //
|
||||
////////////////////
|
||||
qInstance.setAuthentication(new Auth0AuthenticationMetaData()
|
||||
.withType(QAuthenticationType.AUTH_0)
|
||||
// ... auth config ...
|
||||
);
|
||||
|
||||
//////////
|
||||
// CRM //
|
||||
//////////
|
||||
CrmQBitConfig crmConfig = new CrmQBitConfig()
|
||||
.withDefaultBackendNameForTables("rdbms");
|
||||
|
||||
new CrmQBitProducer()
|
||||
.withQBitConfig(crmConfig)
|
||||
.produce(qInstance)
|
||||
.addSelfToInstance(qInstance);
|
||||
|
||||
//////////
|
||||
// App //
|
||||
//////////
|
||||
qInstance.addApp(new QAppMetaData()
|
||||
.withName("myApp")
|
||||
.withSections(List.of(
|
||||
CrmQBitProducer.produceAppSection()
|
||||
)));
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
## Data Model
|
||||
|
||||
```java
|
||||
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");
|
||||
### Tables (39 total)
|
||||
|
||||
#### Core (Phase 1a)
|
||||
|
||||
| Table | Description |
|
||||
|-------|-------------|
|
||||
| `crmContact` | People with lifecycle stages, lead scoring, consent tracking |
|
||||
| `crmCompany` | Organizations with industry, type, parent-child hierarchy |
|
||||
| `crmLifecycleStage` | Configurable contact lifecycle (Lead, MQL, SQL, Customer, etc.) |
|
||||
| `crmLeadSource` | How contacts were acquired (Website, Referral, etc.) |
|
||||
| `crmIndustry` | Industry classification for companies |
|
||||
| `crmTag` | Free-form tags for categorizing records |
|
||||
| `crmContactTag` / `crmCompanyTag` | Tag junction tables |
|
||||
| `crmAttachment` | File attachments on any entity (polymorphic) |
|
||||
| `crmConsentRecord` | GDPR consent tracking (append-only) |
|
||||
| `crmFormSubmission` | Website form capture with UTM parameters |
|
||||
| `crmEmailList` | Marketing mailing lists |
|
||||
| `crmEmailListMember` | List subscribers (standalone or linked to contacts) |
|
||||
| `crmCurrency` | Exchange rates for multi-currency support |
|
||||
| `crmAuditLog` | Immutable field-level change tracking |
|
||||
|
||||
#### Activities
|
||||
|
||||
| Table | Description |
|
||||
|-------|-------------|
|
||||
| `crmActivity` | Unified table for calls, emails, meetings, notes, tasks |
|
||||
| `crmActivityType` | Configurable activity types with icons |
|
||||
| `crmActivityOutcome` | Call/meeting outcomes (Connected, No Answer, etc.) |
|
||||
| `crmActivityParticipant` | Additional participants on activities |
|
||||
|
||||
#### Deals & Pipeline (Phase 1b)
|
||||
|
||||
| Table | Description |
|
||||
|-------|-------------|
|
||||
| `crmPipeline` | Pipeline definitions (multiple pipelines supported) |
|
||||
| `crmPipelineStage` | Stages with probability, rot days, required-field gating |
|
||||
| `crmDeal` | Deals with multi-currency, weighted amounts, probability override |
|
||||
| `crmDealContact` | Contact-to-deal junction with roles and primary flag |
|
||||
| `crmContactRole` | Roles contacts play on deals (Decision Maker, etc.) |
|
||||
| `crmWinLossReason` | Win/loss reasons for closed deals |
|
||||
| `crmDealTag` | Deal tag junction |
|
||||
| `crmDealStageHistory` | Stage transition log for pipeline analytics |
|
||||
|
||||
#### Email (Phase 2)
|
||||
|
||||
| Table | Description |
|
||||
|-------|-------------|
|
||||
| `crmEmailTemplate` | Reusable templates with merge field support |
|
||||
| `crmEmailMessage` | Tracked emails with open/click/bounce/spam detection |
|
||||
|
||||
#### Products (Phase 2)
|
||||
|
||||
| Table | Description |
|
||||
|-------|-------------|
|
||||
| `crmProduct` | Product catalog with SKU and pricing |
|
||||
| `crmDealProduct` | Line items on deals with quantity, discount, calculated totals |
|
||||
|
||||
#### Sequences & Scoring (Phase 3)
|
||||
|
||||
| Table | Description |
|
||||
|-------|-------------|
|
||||
| `crmEmailSequence` | Multi-step outreach sequences with send windows |
|
||||
| `crmSequenceStep` | Individual steps (email, call, task, wait) |
|
||||
| `crmSequenceEnrollment` | Contact enrollment tracking with failure handling |
|
||||
| `crmLeadScoreRule` | Scoring rules with dot-notation field paths |
|
||||
| `crmAssignmentRule` | Auto-assignment rules (round-robin, least-active, specific user) |
|
||||
| `crmAssignmentRuleMember` | Users in assignment pools |
|
||||
| `crmSalesGoal` | Revenue quotas per user per period |
|
||||
|
||||
#### Sync (Phase 2)
|
||||
|
||||
| Table | Description |
|
||||
|-------|-------------|
|
||||
| `crmExternalMapping` | Bidirectional sync mapping for external systems |
|
||||
|
||||
### Enum PossibleValueSources (24)
|
||||
|
||||
CrmEntityType, CrmCompanyType, CrmDealPriority, CrmActivityPriority, CrmDirection, CrmBounceType, CrmWinLossType, CrmParticipantRole, CrmSequenceStepType, CrmEnrollmentStatus, CrmEmailStatus, CrmScorableEntity, CrmScoreOperator, CrmAssignableEntity, CrmAssignStrategy, CrmConsentType, CrmConsentStatus, CrmLegalBasis, CrmGoalPeriod, CrmSyncStatus, CrmAuditAction, CrmFormType, CrmFormConversionStatus, CrmListMemberStatus
|
||||
|
||||
## Processes (27)
|
||||
|
||||
### Core Operations
|
||||
|
||||
| Process | Description |
|
||||
|---------|-------------|
|
||||
| `logActivity` | Quick-log a call, meeting, note, or task from any record |
|
||||
| `convertFormToContact` | Convert website form submission to CRM contact |
|
||||
| `bulkConvertForms` | Batch convert multiple form submissions |
|
||||
| `subscribeToList` / `unsubscribeFromList` | Mailing list management |
|
||||
| `bulkAssignOwner` | Reassign record ownership in bulk |
|
||||
| `bulkAddTag` / `bulkRemoveTag` | Tag management in bulk |
|
||||
| `transferOwnership` | Transfer all records from one user to another |
|
||||
|
||||
### Deal Operations
|
||||
|
||||
| Process | Description |
|
||||
|---------|-------------|
|
||||
| `moveDealStage` | Move deal to new stage with required-field validation |
|
||||
| `reopenDeal` | Reopen a closed deal to an active stage |
|
||||
| `convertLead` | Convert contact to deal (atomic: lifecycle + deal + junction) |
|
||||
| `manageContactOnDeal` | Add/remove contacts with role and primary flag enforcement |
|
||||
| `cloneDeal` | Duplicate deal with contacts, products, and tags |
|
||||
|
||||
### Email & Sequences
|
||||
|
||||
| Process | Description |
|
||||
|---------|-------------|
|
||||
| `sendEmail` | Compose and send tracked email with template resolution |
|
||||
| `trackEmailEvent` | Webhook callback for open/click/bounce/spam events |
|
||||
| `enrollInSequence` | Enroll contacts in multi-step sequences |
|
||||
| `processSequenceSteps` | Scheduled: execute pending sequence steps (5 min interval) |
|
||||
| `unenrollFromSequence` | Remove contact from active sequence |
|
||||
|
||||
### Scoring & Assignment
|
||||
|
||||
| Process | Description |
|
||||
|---------|-------------|
|
||||
| `calculateLeadScores` | Scheduled daily: recalculate scores from rules |
|
||||
| `autoAssign` | POST_INSERT hook: first-match rule-based assignment |
|
||||
|
||||
### Data Management
|
||||
|
||||
| Process | Description |
|
||||
|---------|-------------|
|
||||
| `mergeContacts` / `mergeCompanies` | Merge duplicates with unique-key conflict resolution |
|
||||
| `importContacts` | CSV import with validation and duplicate detection |
|
||||
| `detectDuplicates` | Find duplicate contacts by email or companies by domain |
|
||||
| `gdprAnonymizeContact` | Right-to-erasure: anonymize PII, scrub audit log, delete attachments |
|
||||
|
||||
### Scheduled Maintenance
|
||||
|
||||
| Process | Description |
|
||||
|---------|-------------|
|
||||
| `processOverdueTasks` | Hourly: detect overdue task activities |
|
||||
| `fireReminders` | Every 5 min: fire and clear activity reminders |
|
||||
|
||||
## Widgets (14)
|
||||
|
||||
| Widget | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `crmPipelineSummary` | Table | Deal count and amount per stage |
|
||||
| `crmActivityFeed` | Table | Most recent activities across all entities |
|
||||
| `crmDealForecast` | Table | Weighted pipeline by expected close month |
|
||||
| `crmRottingDeals` | Table | Deals exceeding stage rot threshold |
|
||||
| `crmContactTimeline` | Stepper | Per-contact activity timeline |
|
||||
| `crmActivityCount` | Statistics | Activity counts by type (today/week/month) |
|
||||
| `crmRecentDeals` | Table | Recently won/lost deals |
|
||||
| `crmSalesLeaderboard` | Table | Users ranked by closed-won revenue |
|
||||
| `crmQuotaAttainment` | Statistics | Won revenue vs. sales goal target |
|
||||
| `crmPipelineConversion` | Table | Stage-to-stage conversion rates |
|
||||
| `crmStageDuration` | Table | Average days per pipeline stage |
|
||||
| `crmWinRate` | Table | Win rate trend over time |
|
||||
| `crmAvgDealSize` | Table | Average won deal size |
|
||||
| `crmSalesCycleLength` | Table | Average days from creation to close |
|
||||
|
||||
## Companion QBit Integration
|
||||
|
||||
### qbit-quick-search (ElasticSearch)
|
||||
|
||||
Index `crmContact`, `crmCompany`, and `crmDeal` for full-text global search.
|
||||
|
||||
### qbit-webhooks
|
||||
|
||||
Supported event types:
|
||||
|
||||
| Event | Fires When |
|
||||
|-------|------------|
|
||||
| `crm.contact.created` / `crm.contact.updated` | Contact inserted or updated |
|
||||
| `crm.company.created` / `crm.company.updated` | Company inserted or updated |
|
||||
| `crm.deal.created` / `crm.deal.stageChanged` | Deal created or stage changed |
|
||||
| `crm.deal.won` / `crm.deal.lost` | Deal moved to terminal stage |
|
||||
| `crm.activity.created` | Activity logged |
|
||||
| `crm.consent.changed` | Consent granted or withdrawn |
|
||||
| `crm.form.submitted` / `crm.form.converted` | Form submitted or converted to contact |
|
||||
|
||||
### qbit-workflows
|
||||
|
||||
User-defined automation beyond built-in scoring/assignment. The CRM does not re-implement a workflow engine.
|
||||
|
||||
## Phased Delivery
|
||||
|
||||
| Phase | Tables | Processes | Widgets | Focus |
|
||||
|-------|--------|-----------|---------|-------|
|
||||
| 1a | 18 | 7 | 6 | Core + Website Integration |
|
||||
| 1b | 8 | 4 | 3 | Deals + Pipeline |
|
||||
| 2 | 5 | 7 | 1 | Email, Products, Scheduling |
|
||||
| 3 | 7 | 5 | 2 | Sequences, Scoring, Automation |
|
||||
| 4 | 1 | 7 | 5 | Data Management, Analytics, GDPR |
|
||||
| **Total** | **39** | **30** | **14** | |
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
mvn test # Run all 271 tests
|
||||
mvn test -Dtest=BaseTest # Verify QBit produces metadata
|
||||
```
|
||||
|
||||
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.
|
||||
### Coverage
|
||||
|
||||
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.
|
||||
- **91.5% instruction coverage** (Jacoco)
|
||||
- **73 test classes** covering all entities, processes, widgets, and customizers
|
||||
- All tests run in-memory via MemoryRecordStore (no database required)
|
||||
|
||||
```bash
|
||||
mvn org.jacoco:jacoco-maven-plugin:0.8.11:prepare-agent test \
|
||||
org.jacoco:jacoco-maven-plugin:0.8.11:report
|
||||
# Report at: target/site/jacoco/index.html
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- **[QQQ Wiki](https://github.com/Kingsrook/qqq/wiki)** - Framework documentation
|
||||
- **[QBit Development Guide](https://github.com/Kingsrook/qqq/wiki/QBit-Development)** - How QBits work
|
||||
- **Design Spec** - Full data model and architecture in `QRun-IO/specs/2026-03-28-qbit-crm-design-v2.md`
|
||||
|
||||
## Contributing
|
||||
|
||||
QBit CRM is open source and welcomes contributions.
|
||||
|
||||
- **[Report Issues](https://github.com/QRun-IO/qqq/issues)** - Bug reports and feature requests
|
||||
- **[QQQ Contribution Guide](https://github.com/Kingsrook/qqq/wiki/Contribution-Guidelines)** - How to contribute
|
||||
|
||||
## About Kingsrook
|
||||
|
||||
QBit CRM is built by **[Kingsrook](https://qrun.io)** - making engineers more productive through intelligent automation and developer tools.
|
||||
|
||||
- **Website**: [https://qrun.io](https://qrun.io)
|
||||
- **Contact**: [contact@kingsrook.com](mailto:contact@kingsrook.com)
|
||||
- **GitHub**: [https://github.com/QRun-IO](https://github.com/QRun-IO)
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the **Apache License, Version 2.0** - see the [LICENSE](LICENSE) file for details.
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
# Upgrading
|
||||
|
||||
## Starter defaults (Unreleased / 0.2.0)
|
||||
|
||||
The CRM now ships `db/liquibase/qbit-crm/crm-defaults.changelog.sql`, a separate insert-only changelog. Without it,
|
||||
converting a lead fails with "no default pipeline found", new web leads and converted contacts silently get no
|
||||
lifecycle stage, and amounts have no currency prefix.
|
||||
|
||||
What a host must do:
|
||||
|
||||
1. Add one line to its master changelog, after `crm.changelog.sql` and, on tenant-enabled installs, after `crm-tenancy.changelog.sql` (the file checks for `tenant_key` columns, so the order matters):
|
||||
|
||||
```yaml
|
||||
- include: { file: db/liquibase/qbit-crm/crm-defaults.changelog.sql }
|
||||
```
|
||||
|
||||
2. Review what it seeds (see README, "Starter defaults"). Existing configuration is never touched: lists are seeded
|
||||
only while their table is empty, the pipeline only when no pipeline exists, its stages only when it is the only pipeline and no stage or deal exists. Lifecycle stages and the base currency are seeded only on a fresh install (no contact, respectively no deal, yet), because they change what intake and Convert lead assign. Rollbacks are empty.
|
||||
3. Tenant-enabled hosts (the `crm-tenancy` changelog added `tenant_key` columns): the changelog skips those tables
|
||||
(marks them ran), because rows without a tenant key are invisible to tenants. Insert the same rows once per tenant
|
||||
with `tenant_key` set. The Setup card on the CRM home shows what is still missing.
|
||||
4. `crm_currency.rate_last_updated_date` is now written by the CRM when a rate is set or changed and is read-only in
|
||||
the UI. Existing values are kept.
|
||||
5. No schema change. `crm_external_mapping` is documented as not written by the CRM yet.
|
||||
+10
-9
@@ -6,17 +6,18 @@
|
||||
~ contact@kingsrook.com
|
||||
~ https://github.com/Kingsrook/
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU Affero General Public License as
|
||||
~ published by the Free Software Foundation, either version 3 of the
|
||||
~ License, or (at your option) any later version.
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU Affero General Public License for more details.
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
~ You should have received a copy of the GNU Affero General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<!DOCTYPE module PUBLIC
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
@@ -9,7 +9,7 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
||||
-475
@@ -1,475 +0,0 @@
|
||||
# QBit CRM: design (0.2.0)
|
||||
|
||||
This is the design for qbit-crm 0.2.0, a full-featured CRM for QQQ 4.1 applications. It also
|
||||
works with **qbit-marketing** and with a host CMS such as the kof22 website (`cms_` tables,
|
||||
the `/v2/contact` form and newsletter capture). The document records three things:
|
||||
- the gap audit of 0.1.0
|
||||
- the target design
|
||||
- the integration contracts and the delivery phases
|
||||
|
||||
0.2.0 is a breaking, fresh-install release: tables are renamed, and several bespoke features are
|
||||
replaced by platform features. 0.1.0 never went live anywhere (Voyage was a proof of concept), so
|
||||
there is no upgrade path or compatibility layer.
|
||||
|
||||
## Goals
|
||||
|
||||
- **The whole sales and relationship workflow in one app:**
|
||||
- contacts and companies
|
||||
- deals through pipelines
|
||||
- logged activities
|
||||
- lists and consent
|
||||
- web leads with attribution
|
||||
- sequences
|
||||
- scoring and assignment
|
||||
- goals
|
||||
- reporting
|
||||
- **Standard QQQ throughout.** Every record is in a visible `crm_` table with a full record
|
||||
screen. Work runs as table and record actions on the standard streamed ETL. The app has one
|
||||
real home.
|
||||
- **Reuse the platform instead of rebuilding it:**
|
||||
- email through qbit-email or core messaging
|
||||
- tags, comments, attachments, follow and tasks through collaboration
|
||||
- notifications through alerts
|
||||
- long work as durable jobs
|
||||
- imports through bulk load with saved profiles
|
||||
- reporting through data analysis
|
||||
- PII through secured data
|
||||
- **Honest consent.** Nobody gets marketing email without a recorded, unexpired Email consent.
|
||||
Every suppression is respected and every change is auditable.
|
||||
- **Integrations through published contracts only.** The `com.kingsrook.qbits.crm.api` package
|
||||
is the only thing other qbits and hosts compile against (guardrail G1).
|
||||
|
||||
## Gap audit of 0.1.0 (develop @ 547c923)
|
||||
|
||||
### What 0.1.0 claims versus what works
|
||||
|
||||
| Area | README claim | Actual state | 0.2.0 action |
|
||||
|---|---|---|---|
|
||||
| App and nav | CRM section links every table, 14 dashboard widgets | `produceAppSection()` returns an empty section; no app, no dashboard; the widgets are on nothing | One `crm` app, real home, main tables only (see [App](#app-home-and-nav)) |
|
||||
| Email sending | Send, track opens/clicks/bounces | Nothing is sent. `sendEmail` writes an activity and a `SENT` message row; `fromAddress` is a user id; templates have no merge. Sequence email steps only log an activity | Send through qbit-email `EmailApi` or core `SendMessageAction`; delivery state from qbit-email |
|
||||
| Scheduled maintenance | Sequences every 5 min, scoring hourly, overdue tasks daily | No schedules exist; nothing runs unless the host adds jobs | Shipped schedules that run as durable jobs |
|
||||
| Auto-assign | POST_INSERT hook | A standalone process only; never triggered | Insert customizer on contact and deal plus the record action |
|
||||
| Lead score decay | `DAYS_AGO_GT/LT` operators | Enum values exist, evaluation returns false | Implemented |
|
||||
| Reminders | Reminder notifications | `fireReminders` clears the flag ("for now") | Collaboration tasks and alerts notifications |
|
||||
| Import | CSV import with dedup | Needs pre-parsed records; loads every contact into memory | Core bulk load with saved profiles; dedup by unique key and the Find Duplicates action |
|
||||
| Multi-tenant | Locks on all tables, propagated to children | Locks on contact, company and deal only; no child propagation; widgets and processes query unscoped | Lock on every table (direct or by join chain), configurable key type |
|
||||
| Permissions | Not stated | No permission rules at all | `crm.*` permission rules on every table and action |
|
||||
| Webhooks, quick search, workflows | Config namespaces | Dead config, never read | Removed. Search via table search fields; events via ESB |
|
||||
| GDPR erase | Anonymizes a contact | Leaves PII in email messages, form submissions, list members, activities, consent IP, external mappings | Erase covers every table that holds the contact's PII |
|
||||
| Audit | Field-level audit | Bespoke `crmAuditLog` on 4 tables, stores PII copies | Core table audit; erase redacts audit details |
|
||||
| Tags, attachments | Bulk tag management, attachments | Junction tables; attachments are metadata only, with no storage | Collaboration TAGS and ATTACHMENTS (object storage) |
|
||||
| `bulkConvertForms` | Listed | Does not exist | Convert to Contact is a multi-record action |
|
||||
| Tests | 73 classes / 271 tests | 81 classes / 296 tests, memory backend only | Add RDBMS (H2) tests for DDL, unique keys and snake_case columns |
|
||||
|
||||
These parts are real and are kept, adapted to 0.2.0:
|
||||
- pipelines and stages with required-field gating and stage history
|
||||
- deal products with total recalculation
|
||||
- multi-currency conversion
|
||||
- merge contacts and merge companies
|
||||
- sequence timing (business days, send windows, contact time zone)
|
||||
- the assignment strategies (specific user, round robin, least active)
|
||||
- scoring operators 1-7
|
||||
- form-submission auto-linking
|
||||
- list subscribe and unsubscribe
|
||||
- the 24 enum possible-value sources
|
||||
- the three contingent dropdowns from #6
|
||||
|
||||
### Against the current platform
|
||||
|
||||
| Standard | 0.1.0 | 0.2.0 |
|
||||
|---|---|---|
|
||||
| Build | parent 1.4.0, QQQ 0.27.9, Java 17, orb 0.3.8 | parent 2.1.2 (guardrails profile), QQQ 4.1.0-RC.6 (RC.7 when cut), Java 21, `./mvnw`, orb 0.6.9 |
|
||||
| Table names | `crmContact` (camelCase) | `crm_contact`, backend name = table name, snake_case columns |
|
||||
| Permissions | none | `crm`, `crm.manage`, `crm.admin` bases (see [Security](#security-tenancy-and-pii)) |
|
||||
| UI | n/a | `/qqq/v1` only; the one public machine route is under `/qqq/hooks/crm/` (G2, G3) |
|
||||
| Messaging | own (fake) sending | qbit-email public API (allow-listed by G1) or core `SendMessageAction` |
|
||||
| Collaboration | bespoke tags, attachments, reminders | core `RecordFeaturesMetaData` (TAGS, COMMENTS, ATTACHMENTS, FOLLOW, TASKS, ASSIGNMENT, TIMELINE) |
|
||||
| Alerts | none | `QAlerts` producer SPI, `crm.*` alert types |
|
||||
| Heavy work | synchronous steps over whole tables | durable jobs (`DurableProcessMetaData`), streamed ETL, keyset pages |
|
||||
| Imports | `importContacts` | core bulk load + saved bulk-load profiles |
|
||||
| PII | none | `DataClassification.PII` on every personal field; secured-data tiers apply |
|
||||
| Reporting | 14 bespoke widgets | home widgets for the daily view; trend reports via data analysis (`crm_*.analyze`) |
|
||||
| Health | none | `crm` health checks (stand-in until core #1199) |
|
||||
| Help | none | `help/qbit-crm.yaml` bundle (stand-in loader until core #1198) |
|
||||
| Tenancy | clientId lock on 3 tables | configurable tenant key type on every table; the host maps Authentik claims to keys |
|
||||
|
||||
## Data model (33 tables: 6 main, 15 settings, 12 sub-tables)
|
||||
|
||||
All names are `crm_` snake_case. Fields stay camelCase in Java and snake_case in the database
|
||||
(`inferBackendNames`). Each table has a reference DDL in `docs/ddl/postgres.sql` and a
|
||||
Liquibase changelog in `src/main/resources/db/liquibase/qbit-crm/`.
|
||||
|
||||
**Main tables** (in the nav; ESB events, quick search, API):
|
||||
|
||||
| Table | Holds |
|
||||
|---|---|
|
||||
| `crm_contact` | Person: names, emails, phones, address, title, company, owner, lifecycle stage, lead source, lead score, time zone, `doNotEmail` / `doNotCall`, unsubscribe date, first- and last-touch attribution (UTM source, medium, campaign, landing page, referrer, touch dates), last activity date. |
|
||||
| `crm_company` | Organization: name, domain, industry, type, size, revenue, parent company, owner, address, last activity date. |
|
||||
| `crm_deal` | Opportunity: pipeline, stage, amount, currency, base-currency amount, weighted amount, probability, expected close, owner, company, primary contact, priority, win/loss reason, stage entered date, attribution copied from the contact at creation. |
|
||||
| `crm_activity` | Logged interaction (call, meeting, email, note, sequence step) with type, outcome, direction, times, related contact, company and deal. Future to-dos are collaboration tasks, not activities. |
|
||||
| `crm_form_submission` | One web form submission: form key and type, name, email, phone, company, message, consent given and its text, UTM fields, landing page, referrer, visitor key (hashed), contact link, conversion status. Raw IP is never stored; only a salted client hash. |
|
||||
| `crm_email_list` | Mailing list: key, name, description, double opt-in flag, consent wording, active. |
|
||||
|
||||
**Settings tables** (Settings section of the nav):
|
||||
|
||||
`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** (child lists on their parents, not in the nav):
|
||||
|
||||
| Table | Parent | Notes |
|
||||
|---|---|---|
|
||||
| `crm_pipeline_stage` | pipeline | Probability, rot days, required fields, won/lost flags. |
|
||||
| `crm_consent_record` | contact | Append-only: type, status, legal basis, source, wording shown, evidence (form submission), recorded and expiry dates. |
|
||||
| `crm_email_list_member` | list, contact | Status (pending confirmation, subscribed, unsubscribed, bounced), confirmation token hash, dates, source submission. |
|
||||
| `crm_email_message` | contact, deal | CRM view of a sent email: template, subject, status, the qbit-email message id and idempotency key, sent/opened/clicked/bounced dates. Bodies stay in qbit-email. |
|
||||
| `crm_sequence_step` | sequence | |
|
||||
| `crm_sequence_enrollment` | sequence, contact | Status, current step, next step date, failures. |
|
||||
| `crm_deal_contact` | deal | Role on the deal. |
|
||||
| `crm_deal_product` | deal | Line items. |
|
||||
| `crm_deal_stage_history` | deal | Read-only. |
|
||||
| `crm_activity_participant` | activity | |
|
||||
| `crm_assignment_rule_member` | assignment rule | |
|
||||
| `crm_external_mapping` | any | External system ids for sync connectors. |
|
||||
|
||||
**Removed:**
|
||||
|
||||
| Removed | Replaced by |
|
||||
|---|---|
|
||||
| `crmTag`, `crmContactTag`, `crmCompanyTag`, `crmDealTag` | Collaboration TAGS |
|
||||
| `crmAttachment` | Collaboration ATTACHMENTS (object storage) |
|
||||
| `crmAuditLog` | Core table audit (`withAuditRules`, FIELD level) |
|
||||
|
||||
## App, home and nav
|
||||
|
||||
There is one app, `crm` ("CRM"). Its home is the only CRM dashboard. All widgets are standard,
|
||||
full-width and real-data:
|
||||
|
||||
1. **Today** (statistics row):
|
||||
- open pipeline (base currency)
|
||||
- won this month
|
||||
- new contacts (30 days)
|
||||
- web leads (7 days)
|
||||
- my overdue tasks
|
||||
2. **Pipeline by stage**: stage, deals, amount and weighted amount. The first pipeline is the
|
||||
default; a pipeline picker is added when there is more than one.
|
||||
3. **Forecast**: expected close by month for the next 6 months, weighted.
|
||||
4. **Needs attention**: rotting deals (days in stage past the stage's `rotDays`).
|
||||
5. **Recent web leads**: the last 10 form submissions, with their contact and conversion status.
|
||||
6. **Goals**: attainment for the current period. It is shown only when goals exist.
|
||||
7. **Leaderboard**: won amount by owner this quarter.
|
||||
|
||||
Every table widget has at most 3 columns, and each column links to its records. On a contact:
|
||||
- a **Timeline** child widget (activities, messages, submissions, consent, in date order)
|
||||
- the child lists for deals, lists, consent and enrollments
|
||||
|
||||
On a deal:
|
||||
- stage history
|
||||
- line items
|
||||
- contacts
|
||||
|
||||
The nav has two parts:
|
||||
- **Main tables:** contacts, companies, deals, activities, web leads (`crm_form_submission`) and
|
||||
lists.
|
||||
- **Settings:** the settings tables.
|
||||
|
||||
Processes are never in the nav. The trend reports from 0.1.0 (win rate, stage duration, sales
|
||||
cycle, conversion, average deal size) become data-analysis analyses. Deals carry the computed
|
||||
fields `daysInStage` and `cycleDays`, and the analyses are seeded as saved analyses when
|
||||
qbit-data-analysis is installed. Without it, they are table views with group-by. They are not
|
||||
custom widgets.
|
||||
|
||||
## Actions
|
||||
|
||||
Every action is a table or record action with a plain label. Each runs on
|
||||
`StreamedETLWithFrontendProcess` with the standard Review and Result summary lines, and has no
|
||||
custom dry-run forms. Large selections run as durable jobs.
|
||||
|
||||
| Table | Actions |
|
||||
|---|---|
|
||||
| Contact | Assign Owner, Auto-Assign, Add to List, Remove from List, Enroll in Sequence, Unenroll, Send Email, Log Activity, Recalculate Score, Merge (2+ selected), Find Duplicates, Erase Personal Data (GDPR, `crm.admin`), Record Consent |
|
||||
| Company | Assign Owner, Merge, Find Duplicates, Log Activity |
|
||||
| Deal | Move Stage, Mark Won, Mark Lost, Reopen, Clone, Assign Owner, Add Contact, Log Activity |
|
||||
| Web lead | Convert to Contact (multi-record), Mark Spam |
|
||||
| List | Send Confirmation Again (pending members) |
|
||||
| Sequence | Activate, Pause |
|
||||
| Email message | Refresh Delivery Status |
|
||||
|
||||
**Scheduled work**, shipped as `QScheduleMetaData` (the host can turn each one off in config).
|
||||
Each runs as a durable job:
|
||||
|
||||
| Job | Schedule | Work |
|
||||
|---|---|---|
|
||||
| Run Sequence Steps | every 5 min | Due enrollments, in pages of 200 |
|
||||
| Recalculate Lead Scores | hourly | Contacts whose inputs changed since the last run, plus a full pass nightly |
|
||||
| Sync Email Delivery | every 15 min | qbit-email status for messages sent by the CRM in the last 30 days. Hard bounces and complaints set `doNotEmail` and add a consent record |
|
||||
| Expire Consents | daily | Expired consents and stale pending confirmations (14 days) |
|
||||
|
||||
## Platform reuse
|
||||
|
||||
### Email
|
||||
|
||||
Email sending goes in this order:
|
||||
1. **qbit-email, when installed.** `CrmEmailTransport` calls `EmailApi.sendOnce` with:
|
||||
- category MARKETING for list and sequence mail, or TRANSACTIONAL for one-to-one mail
|
||||
- idempotency key `crm:<purpose>:<id>`
|
||||
- a linked record (the contact)
|
||||
|
||||
The transport is the only class that references `com.kingsrook.qbits.email.api`. That
|
||||
package is allow-listed by G1, and the dependency is optional. qbit-email owns branding,
|
||||
tracking, the transport-level suppression and RFC 8058 one-click unsubscribe.
|
||||
2. **Core `SendMessageAction`, otherwise.** It uses the host's provider name from config.
|
||||
3. **Neither.** Send actions are hidden, and the readiness check reports *Not configured*.
|
||||
|
||||
Templates are `crm_email_template` rows, rendered by qbit-email's templating when available.
|
||||
They have merge fields for:
|
||||
- the contact (`firstName`, `company.name`, ...)
|
||||
- the owner
|
||||
- the unsubscribe link
|
||||
|
||||
Before every marketing send, the CRM checks eligibility: consent, `doNotEmail`, list status and
|
||||
unsubscribe date. qbit-email then applies its own suppression.
|
||||
|
||||
### Collaboration
|
||||
|
||||
The CRM declares `RecordFeaturesMetaData` on contact, company, deal and activity. Features:
|
||||
- TAGS, COMMENTS, ATTACHMENTS, FOLLOW, TIMELINE, RECENTS and FAVORITES on all four
|
||||
- TASKS and ASSIGNMENT on contact, company and deal
|
||||
|
||||
Ownership is `RecordOwnership.withOwnerFieldName("ownerUserId")`. The record screens and
|
||||
"My Work" come from qbit-collaboration when it is installed. Without it, the core declarations
|
||||
are inert.
|
||||
|
||||
### Alerts and notifications
|
||||
|
||||
| Kind | Name | Fires when |
|
||||
|---|---|---|
|
||||
| Alert type | `crm.sequence.failing` | An enrollment fails 3 times |
|
||||
| Alert type | `crm.email.unavailable` | The transport is not ready while sequences are active |
|
||||
| Notification | `crm.lead.assigned` | A new web lead is assigned |
|
||||
| Notification | `crm.deal.rotting` | A deal passes its stage's rot days |
|
||||
| Notification | `crm.deal.won` | A deal is won |
|
||||
| Notification | `crm.task.due` | A collaboration task comes due |
|
||||
|
||||
Notifications go through `QAlerts.notify`. qbit-alerts then delivers each one to the user's
|
||||
channels (in-app, email, Slack) per their notification preferences. Without qbit-alerts, the
|
||||
signals are logged.
|
||||
|
||||
### Durable jobs and bulk load
|
||||
|
||||
- The four scheduled processes, Recalculate Score, Merge and Erase are `DurableProcessMetaData`
|
||||
(eligible). Recalculate and the sequence runner are `alwaysDurable`.
|
||||
- Imports use core bulk load on contact, company, deal and activity. The host registers
|
||||
`SavedBulkLoadProfileMetaDataProvider` once. The CRM ships starter profiles (contacts from a
|
||||
generic CSV, companies, deals). Duplicate emails are refused by the `crm_contact` unique key
|
||||
(email, tenant) and reported in the bulk-load result.
|
||||
|
||||
### Health
|
||||
|
||||
`CrmHealthChecks` reports three checks:
|
||||
- email transport readiness
|
||||
- sequence lag (the oldest due enrollment more than 30 min late)
|
||||
- the scoring job's last success time
|
||||
|
||||
It is a `STAND-IN(core=QRun-IO/qqq#1199, removeBy=4.1.0-RC.9)` adapter onto the
|
||||
`qqq-middleware-health` `HealthIndicator` until the central `HealthCheck` SPI ships.
|
||||
|
||||
### Help
|
||||
|
||||
`src/main/resources/help/qbit-crm.yaml` is in the qqq#1198 bundle format. It has entries for:
|
||||
- the app
|
||||
- every table, field, action and possible-value source
|
||||
|
||||
`HelpBundleAssertions` checks that it is complete. Until `HelpBundles.register` is in a core RC,
|
||||
`CrmHelpBundleStandIn` (`STAND-IN(core=QRun-IO/qqq#1198, removeBy=4.1.0-RC.9)`) loads the same
|
||||
file into `QHelpContent`.
|
||||
|
||||
### Contingent dropdowns
|
||||
|
||||
These dropdowns are contingent on another field:
|
||||
- pipeline stage on pipeline (from #6)
|
||||
- activity outcome on activity type (from #6)
|
||||
- list-member submission on contact (from #6)
|
||||
- deal primary contact on company (`SHOW_ALL_UNTIL_SET`)
|
||||
- deal contact on deal company
|
||||
- sequence step template on nothing (all templates)
|
||||
- sales goal pipeline stage on pipeline
|
||||
|
||||
Server-side validation (`ValidateContingentPossibleValuesHelper`) is on for stage and outcome.
|
||||
|
||||
### API, ESB and search
|
||||
|
||||
- **API:** the host's API name and version come from config. When set, main and settings tables
|
||||
get `ApiTableMetaDataContainer` entries, and sub-tables are reachable as children. The
|
||||
published API matches the admin, and a parity test compares the API field list with the table
|
||||
fields.
|
||||
- **ESB:** main tables carry `EsbTableMetaData` publications to the configured destination, with
|
||||
INSERT, UPDATE and DELETE, and classified values excluded.
|
||||
- **Quick search:** main tables declare `searchFields`, so qbit-quick-search indexes them
|
||||
without a compile dependency.
|
||||
|
||||
## Security, tenancy and PII
|
||||
|
||||
**Permissions** use the full qbit name:
|
||||
|
||||
| Permission | Allows |
|
||||
|---|---|
|
||||
| `crm.hasAccess`, `crm.read`, `crm.write` | The app; main and sub-tables; everyday actions |
|
||||
| `crm.manage.*` | Settings tables (including currencies); merge, bulk assign, list and sequence management |
|
||||
| `crm.admin.*` | Erase personal data and consent overrides |
|
||||
|
||||
Each table's standard QQQ suffixes map onto these, through `QPermissionRules` with
|
||||
`DenyBehavior.HIDDEN`.
|
||||
|
||||
**Tenancy (multi-company).** `CrmQBitConfig.withTenantSecurityKeyType("crmTenant")` (off by
|
||||
default) does three things:
|
||||
- adds a nullable `tenantKey` column and a `RecordSecurityLock` to every main and settings table
|
||||
- locks sub-tables by join chain
|
||||
- tells collaboration to mirror the key type
|
||||
|
||||
The host maps Authentik groups or claims to keys in its `AuthentikPolicyClaimMapper` override.
|
||||
An all-access key is configurable. Widgets, processes and the API query with the session, so
|
||||
every read is scoped. kof22 is single-tenant and leaves this off. The 0.1.0 `securityFields` and
|
||||
`recordSecurityLocks` config is replaced.
|
||||
|
||||
**PII classification** (`DataClassification.PII`):
|
||||
- contact: names, emails, phones, address, LinkedIn, time zone
|
||||
- company: phone and address
|
||||
- form submission: name, email, phone, message, client hash
|
||||
- list member: email and name
|
||||
- email message: addresses and subject
|
||||
- consent: evidence text
|
||||
- activity: description
|
||||
|
||||
With qbit-secured-data, these fields follow its tiers (masked, revealable or hidden), and
|
||||
classified values are left out of ESB and AI tool output.
|
||||
|
||||
**Erase personal data** (GDPR Art. 17) removes or anonymizes everything that names the contact:
|
||||
- the contact's PII fields
|
||||
- form submissions, list memberships and email-message addresses
|
||||
- activity descriptions
|
||||
- external mappings
|
||||
- audit details on the contact's records
|
||||
|
||||
It keeps one consent record, "erased on <date>", with no PII. It runs as a durable job and
|
||||
leaves an alert-free audit line.
|
||||
|
||||
## Integration contracts (`com.kingsrook.qbits.crm.api`)
|
||||
|
||||
The package is published inside the qbit-crm jar. Hosts and qbits call it in-process, and it is
|
||||
the only supported surface.
|
||||
|
||||
**`CrmApi`**
|
||||
- `isInstalled(QInstance)`
|
||||
- `readiness()`
|
||||
|
||||
**`CrmIntakeApi`** (for host websites such as kof22)
|
||||
|
||||
`submitForm(CrmFormRequest)` returns `CrmFormResult(submissionId, contactId, contactCreated)`.
|
||||
- **Request fields:**
|
||||
- form key and type (CONTACT, NEWSLETTER, DEMO, OTHER)
|
||||
- name, email, phone, company, message
|
||||
- `marketingConsent` (boolean) and the exact consent wording shown
|
||||
- UTM fields, landing page, referrer
|
||||
- an optional hashed visitor key and client hash
|
||||
- **The CRM:**
|
||||
- stores the submission
|
||||
- links the contact by email, or creates it (lifecycle "Lead", source from UTM or "Website")
|
||||
- sets first-touch attribution when it is empty and always sets last-touch attribution
|
||||
- logs a "Form submission" activity
|
||||
- auto-assigns the contact
|
||||
- raises `crm.lead.assigned`
|
||||
- **Consent:** a contact form with no consent box is recorded as legitimate interest to reply
|
||||
only. It never grants marketing email. Marketing consent is recorded only from an explicit
|
||||
opt-in, with the wording as evidence.
|
||||
|
||||
`subscribe(CrmSubscribeRequest)` takes a list key, email, name, consent wording, source, UTM and
|
||||
an optional visitor key. It returns PENDING_CONFIRMATION, SUBSCRIBED or ALREADY_SUBSCRIBED.
|
||||
- With double opt-in (the default for lists), it creates a pending member and sends a
|
||||
confirmation email.
|
||||
- The member's consent is granted only by `confirm(token)`.
|
||||
|
||||
`unsubscribe(token)` and `unsubscribeByContact(contactId, listKey-or-null, source)` both record
|
||||
the unsubscribe and a withdrawn consent.
|
||||
|
||||
**`CrmMarketingApi`** (for qbit-marketing; agreed with the marketing agent 2026-10-14)
|
||||
|
||||
| Method | Behavior |
|
||||
|---|---|
|
||||
| `resolveRecipients(scope)` | Scopes `crm-list:<id>` and `crm-contact:<id>`. Returns (contactId, address only when eligible, eligibility). |
|
||||
| `checkEmailEligibility(contactId)` | The latest Email consent decides (granted and not expired; none means no consent). Suppressed by `doNotEmail`, an unsubscribe date, or a non-subscribed membership. Returns reason codes, no PII. |
|
||||
| `listFormSubmissions(utmCampaign, since)` | Ids and times only. |
|
||||
| `recordActivity(contactId, ActivityKind.NOTE, subject, description)` | The CRM picks the activity type by code, so marketing no longer looks up "Note" by name. |
|
||||
| `recordUnsubscribe(contactId, listId-or-null, source)` | Records the unsubscribe. |
|
||||
| `buildUnsubscribeUrl(contactId, listId-or-null)` | A signed, expiring token link to `/qqq/hooks/crm/unsubscribe/{token}` that marketing puts in its footer. The CRM owns list-level unsubscribe; qbit-email's one-click covers its category. |
|
||||
| `recipientScopeOptions()` | Lists and their labels, for marketing's recipient-scope dropdown. |
|
||||
|
||||
**Public route.** `/qqq/hooks/crm/unsubscribe/{token}` (GET shows a confirm page, POST does it)
|
||||
and `/qqq/hooks/crm/confirm/{token}` (double opt-in). These are the only HTTP routes outside
|
||||
`/qqq/v1`, and they live in a route provider. Tokens are HMAC-signed with a host secret from
|
||||
config, expire after 90 days, and are stored only as hashes.
|
||||
|
||||
**Attribution from analytics.** Hosts pass the UTM fields they already capture: for kof22, the
|
||||
`cms_analytics` session's first and last UTM for a consenting visitor, or else the page URL
|
||||
parameters. Without visitor-identity consent, the CRM stores no visitor key.
|
||||
|
||||
## kof22 adoption (Website-Backend)
|
||||
|
||||
The adoption changes:
|
||||
- **Dependency and wiring:** add `qbit-crm` and register it through `WebsiteCrmMetaData.configure`
|
||||
from `WebsiteMetaDataProvider`. It uses the CMS backend, with tenancy off.
|
||||
- **Permissions:** grant them in `AuthentikAuthenticationCustomizer` (admins get `crm.*`).
|
||||
- **Schema:** a dated Liquibase file, `changesets/features/2026-10-14-qbit-crm.sql`. kof22's
|
||||
numbered files stop at 0041, and newer changes use dated files.
|
||||
- **`/v2/contact`:** keeps its honeypot, timing, validation and rate limits, then calls
|
||||
`CrmIntakeApi.submitForm` (form key `kof22-contact`).
|
||||
- The notification to the site owner keeps today's delivery path (qbit-email's outbox when it
|
||||
is installed, otherwise the existing mailer). It links to the CRM web lead.
|
||||
- `cms_contact_message` keeps receiving rows until one release after adoption. Existing rows
|
||||
are copied into `crm_form_submission`, and a later cleanup PR drops the table.
|
||||
- **Newsletter:** new `POST /v2/newsletter/subscribe` (same protections) calls
|
||||
`CrmIntakeApi.subscribe` for list `kof22-newsletter`, with double opt-in. The confirm and
|
||||
unsubscribe links use the CRM routes. The frontend form is a separate Website-Frontend PR.
|
||||
- **Marketing:** qbit-marketing on kof22 uses `QbitCrmGateway`, which delegates to
|
||||
`CrmMarketingApi`.
|
||||
|
||||
## No upgrade path from 0.1.0
|
||||
|
||||
0.1.0 never went live. 0.2.0 is a fresh install: there are no rename scripts and no compatibility shims. These 0.1.0 config options are removed:
|
||||
- `tableNamePrefix`
|
||||
- `quickSearchQBitNamespace`
|
||||
- `webhooksQBitNamespace`
|
||||
- `workflowsQBitNamespace`
|
||||
|
||||
`securityFields` and `recordSecurityLocks` are replaced by `withTenantSecurityKeyType` in P4.
|
||||
|
||||
## Delivery phases
|
||||
|
||||
| Phase | Delivers | Demonstrable result |
|
||||
|---|---|---|
|
||||
| P1 Platform | Parent 2.1.2, QQQ 4.1.0-RC.6, Java 21, orb 0.6.9, guardrails; `crm_` rename with snake_case columns; permission rules; reference DDL and Liquibase; H2 tests | `./mvnw verify` green on the new stack |
|
||||
| P2 Standards | App, home and nav; record actions on streamed ETL; PII classification; contingent dropdowns; API, ESB and search metadata; help bundle; core audit | The CRM app runs in a sample server with the home and record screens |
|
||||
| P3 Intake and contracts | `crm.api` (`CrmIntakeApi`, `CrmMarketingApi`), consent, double opt-in, the unsubscribe and confirm routes, attribution | A form post creates a contact and lead, and marketing resolves eligibility |
|
||||
| P4 Platform reuse | Email transport, collaboration features, alerts, durable jobs and schedules, bulk-load profiles, tenancy, health, GDPR erase coverage, scoring decay, auto-assign hook | Sequences send through qbit-email; imports use saved profiles |
|
||||
| P5 kof22 | Website-Backend draft PR (install, Liquibase, `/v2/contact`, newsletter) | Contact form leads appear in the CRM on a local stack |
|
||||
| P6 Release | Screenshots (home and records, light and dark, 390 and 1440 px), CHANGELOG, "ready: qbit-crm release" | 0.2.0 RC |
|
||||
|
||||
## Owner decisions (2026-10-14): all recommendations accepted (standing approval)
|
||||
|
||||
The owner's standing approval (2026-10-12, and repeated for this program on 2026-10-14)
|
||||
accepts every recommendation above. Specifically:
|
||||
|
||||
| # | Decision |
|
||||
|---|---|
|
||||
| 1 | 0.2.0 is a breaking, fresh-install release with `crm_` names and no compatibility work for 0.1.0. Voyage was a proof of concept (owner, 2026-10-14). |
|
||||
| 2 | Tags, attachments, reminders and audit move to collaboration, alerts and core audit. The bespoke tables are removed. |
|
||||
| 3 | The CRM never sends email itself: it uses qbit-email first, then core messaging. |
|
||||
| 4 | Lists use double opt-in by default. Contact forms never grant marketing consent without an explicit opt-in. |
|
||||
| 5 | The CRM owns list-level unsubscribe and the confirm and unsubscribe routes. qbit-email owns transport suppression. |
|
||||
| 6 | Trend reports move to data analysis. The home keeps the daily-work widgets. |
|
||||
| 7 | Tenancy is a configurable key type, off by default. kof22 runs single-tenant. |
|
||||
| 8 | kof22 keeps `cms_contact_message` for one release after adoption, then drops it. |
|
||||
| 9 | Raw IP addresses are never stored. Only salted client hashes and hashed visitor keys are kept. |
|
||||
| 10 | Stand-ins for central health (#1199) and help bundles (#1198) are allowed, with `removeBy=4.1.0-RC.9`. |
|
||||
@@ -1,655 +0,0 @@
|
||||
-- Copyright 2026 Kingsrook, LLC. Licensed under the Apache License, Version 2.0.
|
||||
-- https://www.apache.org/licenses/LICENSE-2.0
|
||||
-- QBit CRM 0.2.0 P1 PostgreSQL reference schema (34 tables).
|
||||
-- QQQ table names equal SQL names; Java field names map to snake_case columns.
|
||||
-- Unique constraints match QQQ metadata. Relationship columns are indexed;
|
||||
-- foreign-key constraints are deliberately not imposed (the QQQ/host layer owns
|
||||
-- validation and deletion policy, including polymorphic and optional references).
|
||||
-- Optional tenancy: separately include crm-tenancy.changelog.sql with context crm-tenancy.
|
||||
-- With tenancy off, do not include it; this base schema has no tenant_key columns.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_activity (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
activity_type_id INTEGER NOT NULL,
|
||||
subject VARCHAR(500) NOT NULL,
|
||||
description TEXT,
|
||||
contact_id INTEGER,
|
||||
company_id INTEGER,
|
||||
deal_id INTEGER,
|
||||
owner_user_id TEXT NOT NULL,
|
||||
assigned_to_user_id TEXT,
|
||||
start_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
end_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
due_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
completed_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
is_completed BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
priority INTEGER,
|
||||
duration_minutes INTEGER,
|
||||
outcome_id INTEGER,
|
||||
location VARCHAR(500),
|
||||
conference_link VARCHAR(1000),
|
||||
direction INTEGER,
|
||||
is_reminder_set BOOLEAN DEFAULT FALSE,
|
||||
reminder_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
external_calendar_id VARCHAR(500),
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_activity_activity_type_id ON crm_activity (activity_type_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_activity_contact_id ON crm_activity (contact_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_activity_company_id ON crm_activity (company_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_activity_deal_id ON crm_activity (deal_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_activity_owner_user_id ON crm_activity (owner_user_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_activity_assigned_to_user_id ON crm_activity (assigned_to_user_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_activity_outcome_id ON crm_activity (outcome_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_activity_external_calendar_id ON crm_activity (external_calendar_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_activity_outcome (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
activity_type_id INTEGER NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
sort_order INTEGER NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT uq_crm_activity_outcome_1 UNIQUE (activity_type_id, name)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_activity_outcome_activity_type_id ON crm_activity_outcome (activity_type_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_activity_participant (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
activity_id INTEGER NOT NULL,
|
||||
contact_id INTEGER,
|
||||
user_id TEXT,
|
||||
role INTEGER,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_activity_participant_activity_id ON crm_activity_participant (activity_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_activity_participant_contact_id ON crm_activity_participant (contact_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_activity_participant_user_id ON crm_activity_participant (user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_activity_type (
|
||||
code VARCHAR(50) UNIQUE,
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
icon_name VARCHAR(50),
|
||||
is_system BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
sort_order INTEGER NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT uq_crm_activity_type_1 UNIQUE (name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_assignment_rule (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(250) NOT NULL,
|
||||
description TEXT,
|
||||
entity_type INTEGER NOT NULL,
|
||||
criteria_json TEXT,
|
||||
assign_strategy INTEGER NOT NULL,
|
||||
assign_to_user_id TEXT,
|
||||
is_active BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
sort_order INTEGER NOT NULL,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_assignment_rule_assign_to_user_id ON crm_assignment_rule (assign_to_user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_assignment_rule_member (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
assignment_rule_id INTEGER NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
sort_order INTEGER NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
last_assigned_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT uq_crm_assignment_rule_member_1 UNIQUE (assignment_rule_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_assignment_rule_member_assignment_rule_id ON crm_assignment_rule_member (assignment_rule_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_assignment_rule_member_user_id ON crm_assignment_rule_member (user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_company (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(250) NOT NULL,
|
||||
domain VARCHAR(250),
|
||||
company_type INTEGER,
|
||||
industry_id INTEGER,
|
||||
employee_count INTEGER,
|
||||
annual_revenue DECIMAL(20,6),
|
||||
annual_revenue_currency_code VARCHAR(3) DEFAULT 'USD',
|
||||
phone VARCHAR(50),
|
||||
website VARCHAR(500),
|
||||
address_line1 VARCHAR(250),
|
||||
address_line2 VARCHAR(250),
|
||||
city VARCHAR(100),
|
||||
state VARCHAR(100),
|
||||
postal_code VARCHAR(20),
|
||||
country VARCHAR(100),
|
||||
owner_user_id TEXT NOT NULL,
|
||||
parent_company_id INTEGER,
|
||||
description TEXT,
|
||||
last_activity_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
last_contacted_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_company_industry_id ON crm_company (industry_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_company_owner_user_id ON crm_company (owner_user_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_company_parent_company_id ON crm_company (parent_company_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_consent_record (
|
||||
consent_text TEXT,
|
||||
evidence_form_submission_id INTEGER,
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
contact_id INTEGER NOT NULL,
|
||||
consent_type INTEGER NOT NULL,
|
||||
status INTEGER NOT NULL,
|
||||
legal_basis INTEGER,
|
||||
source VARCHAR(250),
|
||||
consent_date TIMESTAMP WITHOUT TIME ZONE NOT NULL,
|
||||
expiry_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_consent_record_contact_id ON crm_consent_record (contact_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_contact (
|
||||
first_touch_utm_source VARCHAR(500),
|
||||
first_touch_utm_medium VARCHAR(500),
|
||||
first_touch_utm_campaign VARCHAR(500),
|
||||
first_touch_utm_content VARCHAR(500),
|
||||
first_touch_utm_term VARCHAR(500),
|
||||
first_touch_landing_page VARCHAR(1000),
|
||||
first_touch_referrer VARCHAR(1000),
|
||||
first_touch_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
last_touch_utm_source VARCHAR(500),
|
||||
last_touch_utm_medium VARCHAR(500),
|
||||
last_touch_utm_campaign VARCHAR(500),
|
||||
last_touch_utm_content VARCHAR(500),
|
||||
last_touch_utm_term VARCHAR(500),
|
||||
last_touch_landing_page VARCHAR(1000),
|
||||
last_touch_referrer VARCHAR(1000),
|
||||
last_touch_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
first_name VARCHAR(100),
|
||||
last_name VARCHAR(100),
|
||||
email VARCHAR(320),
|
||||
secondary_email VARCHAR(320),
|
||||
phone VARCHAR(50),
|
||||
mobile_phone VARCHAR(50),
|
||||
job_title VARCHAR(150),
|
||||
department VARCHAR(100),
|
||||
company_id INTEGER,
|
||||
lifecycle_stage_id INTEGER,
|
||||
lead_source_id INTEGER,
|
||||
owner_user_id TEXT,
|
||||
address_line1 VARCHAR(250),
|
||||
address_line2 VARCHAR(250),
|
||||
city VARCHAR(100),
|
||||
state VARCHAR(100),
|
||||
postal_code VARCHAR(20),
|
||||
country VARCHAR(100),
|
||||
timezone VARCHAR(50),
|
||||
preferred_language VARCHAR(10),
|
||||
website VARCHAR(500),
|
||||
linkedin_url VARCHAR(500),
|
||||
description TEXT,
|
||||
lead_score INTEGER DEFAULT 0,
|
||||
do_not_email BOOLEAN DEFAULT FALSE,
|
||||
do_not_call BOOLEAN DEFAULT FALSE,
|
||||
unsubscribe_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
last_activity_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
last_contacted_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_contact_company_id ON crm_contact (company_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_contact_lifecycle_stage_id ON crm_contact (lifecycle_stage_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_contact_lead_source_id ON crm_contact (lead_source_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_contact_owner_user_id ON crm_contact (owner_user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_contact_role (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
sort_order INTEGER NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT uq_crm_contact_role_1 UNIQUE (name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_currency (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
currency_code VARCHAR(3) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
symbol VARCHAR(5),
|
||||
exchange_rate_to_base DECIMAL(20,6) DEFAULT 1.0 NOT NULL,
|
||||
is_base_currency BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
rate_last_updated_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT uq_crm_currency_1 UNIQUE (currency_code)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_deal (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(250) NOT NULL,
|
||||
pipeline_id INTEGER NOT NULL,
|
||||
pipeline_stage_id INTEGER NOT NULL,
|
||||
amount DECIMAL(20,6),
|
||||
currency_code VARCHAR(3) DEFAULT 'USD',
|
||||
amount_in_base_currency DECIMAL(20,6),
|
||||
probability_override_pct INTEGER,
|
||||
weighted_amount DECIMAL(20,6),
|
||||
company_id INTEGER,
|
||||
primary_contact_id INTEGER,
|
||||
owner_user_id TEXT,
|
||||
lead_source_id INTEGER,
|
||||
expected_close_date DATE,
|
||||
actual_close_date DATE,
|
||||
win_loss_reason_id INTEGER,
|
||||
stage_entered_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
description TEXT,
|
||||
priority INTEGER,
|
||||
last_activity_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_deal_pipeline_id ON crm_deal (pipeline_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_deal_pipeline_stage_id ON crm_deal (pipeline_stage_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_deal_primary_contact_id ON crm_deal (primary_contact_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_deal_company_id ON crm_deal (company_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_deal_owner_user_id ON crm_deal (owner_user_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_deal_lead_source_id ON crm_deal (lead_source_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_deal_win_loss_reason_id ON crm_deal (win_loss_reason_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_deal_contact (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
deal_id INTEGER NOT NULL,
|
||||
contact_id INTEGER NOT NULL,
|
||||
contact_role_id INTEGER,
|
||||
is_primary BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT uq_crm_deal_contact_1 UNIQUE (deal_id, contact_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_deal_contact_deal_id ON crm_deal_contact (deal_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_deal_contact_contact_id ON crm_deal_contact (contact_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_deal_contact_contact_role_id ON crm_deal_contact (contact_role_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_deal_product (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
deal_id INTEGER NOT NULL,
|
||||
product_id INTEGER NOT NULL,
|
||||
quantity DECIMAL(20,6) DEFAULT 1 NOT NULL,
|
||||
unit_price DECIMAL(20,6) NOT NULL,
|
||||
discount_pct DECIMAL(20,6) DEFAULT 0,
|
||||
total_amount DECIMAL(20,6),
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_deal_product_deal_id ON crm_deal_product (deal_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_deal_product_product_id ON crm_deal_product (product_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_deal_stage_history (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
deal_id INTEGER NOT NULL,
|
||||
from_stage_id INTEGER,
|
||||
to_stage_id INTEGER NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
transition_date TIMESTAMP WITHOUT TIME ZONE NOT NULL,
|
||||
duration_in_from_stage_days INTEGER,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_deal_stage_history_deal_id ON crm_deal_stage_history (deal_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_deal_stage_history_from_stage_id ON crm_deal_stage_history (from_stage_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_deal_stage_history_to_stage_id ON crm_deal_stage_history (to_stage_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_deal_stage_history_user_id ON crm_deal_stage_history (user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_email_list (
|
||||
list_key VARCHAR(250) NOT NULL UNIQUE,
|
||||
require_double_opt_in BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
consent_text TEXT,
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(250) NOT NULL,
|
||||
slug VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
is_active BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
is_public BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
sort_order INTEGER NOT NULL,
|
||||
member_count INTEGER DEFAULT 0,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT uq_crm_email_list_1 UNIQUE (name),
|
||||
CONSTRAINT uq_crm_email_list_2 UNIQUE (slug)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_email_list_member (
|
||||
confirmation_token_hash VARCHAR(64),
|
||||
confirmation_sent_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
confirmed_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
source VARCHAR(250),
|
||||
consent_text TEXT,
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
email_list_id INTEGER NOT NULL,
|
||||
contact_id INTEGER,
|
||||
email VARCHAR(320) NOT NULL,
|
||||
first_name VARCHAR(100),
|
||||
status INTEGER DEFAULT 1 NOT NULL,
|
||||
subscribed_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
unsubscribed_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
subscription_source VARCHAR(250),
|
||||
form_submission_id INTEGER,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT uq_crm_email_list_member_1 UNIQUE (email_list_id, email)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_email_list_member_email_list_id ON crm_email_list_member (email_list_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_email_list_member_contact_id ON crm_email_list_member (contact_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_email_list_member_form_submission_id ON crm_email_list_member (form_submission_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_email_message (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
idempotency_key VARCHAR(250) UNIQUE,
|
||||
email_message_id VARCHAR(100),
|
||||
delivered_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
bounced_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
activity_id INTEGER,
|
||||
contact_id INTEGER,
|
||||
from_address VARCHAR(250),
|
||||
from_name VARCHAR(100),
|
||||
to_addresses TEXT NOT NULL,
|
||||
cc_addresses TEXT,
|
||||
bcc_addresses TEXT,
|
||||
subject VARCHAR(500) NOT NULL,
|
||||
body_html TEXT,
|
||||
body_text TEXT,
|
||||
sent_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
thread_id VARCHAR(250),
|
||||
email_template_id INTEGER,
|
||||
sequence_enrollment_id INTEGER,
|
||||
direction INTEGER NOT NULL,
|
||||
status INTEGER,
|
||||
is_tracked BOOLEAN DEFAULT TRUE,
|
||||
open_count INTEGER DEFAULT 0,
|
||||
click_count INTEGER DEFAULT 0,
|
||||
first_opened_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
first_clicked_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
bounce_type INTEGER,
|
||||
is_spam_complaint BOOLEAN DEFAULT FALSE,
|
||||
reply_to_email_id INTEGER,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_email_message_email_message_id ON crm_email_message (email_message_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_email_message_contact_id ON crm_email_message (contact_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_email_message_activity_id ON crm_email_message (activity_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_email_message_thread_id ON crm_email_message (thread_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_email_message_email_template_id ON crm_email_message (email_template_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_email_message_sequence_enrollment_id ON crm_email_message (sequence_enrollment_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_email_message_reply_to_email_id ON crm_email_message (reply_to_email_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_email_sequence (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(250) NOT NULL,
|
||||
description TEXT,
|
||||
owner_user_id TEXT NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
total_steps INTEGER,
|
||||
business_days_only BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
send_window_start_hour INTEGER,
|
||||
send_window_end_hour INTEGER,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_email_sequence_owner_user_id ON crm_email_sequence (owner_user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_email_template (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(250) NOT NULL,
|
||||
subject VARCHAR(500) NOT NULL,
|
||||
body_html TEXT NOT NULL,
|
||||
body_text TEXT,
|
||||
category VARCHAR(100),
|
||||
owner_user_id TEXT NOT NULL,
|
||||
is_shared BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_email_template_owner_user_id ON crm_email_template (owner_user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_external_mapping (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
entity_type INTEGER NOT NULL,
|
||||
entity_id INTEGER NOT NULL,
|
||||
external_system VARCHAR(100) NOT NULL,
|
||||
external_id VARCHAR(500) NOT NULL,
|
||||
last_sync_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
sync_status INTEGER,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT uq_crm_external_mapping_1 UNIQUE (entity_type, entity_id, external_system)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_external_mapping_entity_id ON crm_external_mapping (entity_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_external_mapping_external_id ON crm_external_mapping (external_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_form_submission (
|
||||
form_key VARCHAR(250) NOT NULL,
|
||||
marketing_consent BOOLEAN DEFAULT FALSE,
|
||||
consent_text TEXT,
|
||||
landing_page VARCHAR(1000),
|
||||
visitor_key VARCHAR(64),
|
||||
client_hash VARCHAR(64),
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
form_type INTEGER NOT NULL,
|
||||
email VARCHAR(320) NOT NULL,
|
||||
first_name VARCHAR(100),
|
||||
last_name VARCHAR(100),
|
||||
company VARCHAR(250),
|
||||
phone VARCHAR(50),
|
||||
message TEXT,
|
||||
source_url VARCHAR(1000),
|
||||
utm_source VARCHAR(500),
|
||||
utm_medium VARCHAR(500),
|
||||
utm_campaign VARCHAR(500),
|
||||
utm_term VARCHAR(500),
|
||||
utm_content VARCHAR(500),
|
||||
referrer VARCHAR(1000),
|
||||
contact_id INTEGER,
|
||||
conversion_status INTEGER DEFAULT 1 NOT NULL,
|
||||
converted_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
converted_by_user_id TEXT,
|
||||
metadata TEXT,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_form_submission_contact_id ON crm_form_submission (contact_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_form_submission_converted_by_user_id ON crm_form_submission (converted_by_user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_industry (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
sort_order INTEGER NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT uq_crm_industry_1 UNIQUE (name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_lead_score_rule (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(250) NOT NULL,
|
||||
description TEXT,
|
||||
entity_type INTEGER NOT NULL,
|
||||
field_path VARCHAR(250) NOT NULL,
|
||||
operator INTEGER NOT NULL,
|
||||
field_value VARCHAR(500),
|
||||
score_adjustment INTEGER NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
sort_order INTEGER NOT NULL,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_lead_source (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
sort_order INTEGER NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT uq_crm_lead_source_1 UNIQUE (name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_lifecycle_stage (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
sort_order INTEGER NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
description VARCHAR(500),
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT uq_crm_lifecycle_stage_1 UNIQUE (name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_pipeline (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description VARCHAR(500),
|
||||
is_default BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT uq_crm_pipeline_1 UNIQUE (name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_pipeline_stage (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
pipeline_id INTEGER NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
sort_order INTEGER NOT NULL,
|
||||
probability_pct INTEGER DEFAULT 0 NOT NULL,
|
||||
rot_days INTEGER,
|
||||
required_fields_json TEXT,
|
||||
is_closed_won BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
is_closed_lost BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT uq_crm_pipeline_stage_1 UNIQUE (pipeline_id, name),
|
||||
CONSTRAINT uq_crm_pipeline_stage_2 UNIQUE (pipeline_id, sort_order)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_pipeline_stage_pipeline_id ON crm_pipeline_stage (pipeline_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_product (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(250) NOT NULL,
|
||||
sku VARCHAR(100),
|
||||
description TEXT,
|
||||
unit_price DECIMAL(20,6) NOT NULL,
|
||||
currency_code VARCHAR(3) DEFAULT 'USD',
|
||||
is_active BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT uq_crm_product_1 UNIQUE (sku)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_sales_goal (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
pipeline_id INTEGER,
|
||||
period_type INTEGER NOT NULL,
|
||||
period_start DATE NOT NULL,
|
||||
period_end DATE NOT NULL,
|
||||
target_amount DECIMAL(20,6) NOT NULL,
|
||||
currency_code VARCHAR(3) DEFAULT 'USD',
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT uq_crm_sales_goal_1 UNIQUE (user_id, pipeline_id, period_type, period_start)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_sales_goal_user_id ON crm_sales_goal (user_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_sales_goal_pipeline_id ON crm_sales_goal (pipeline_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_sequence_enrollment (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
sequence_id INTEGER NOT NULL,
|
||||
contact_id INTEGER NOT NULL,
|
||||
deal_id INTEGER,
|
||||
current_step_number INTEGER DEFAULT 0 NOT NULL,
|
||||
status INTEGER NOT NULL,
|
||||
enrolled_date TIMESTAMP WITHOUT TIME ZONE NOT NULL,
|
||||
enrolled_by_user_id TEXT NOT NULL,
|
||||
completed_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
unenrolled_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
unenroll_reason VARCHAR(250),
|
||||
next_step_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
failure_count INTEGER DEFAULT 0,
|
||||
last_failure_message VARCHAR(1000),
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT uq_crm_sequence_enrollment_1 UNIQUE (sequence_id, contact_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_sequence_enrollment_sequence_id ON crm_sequence_enrollment (sequence_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_sequence_enrollment_contact_id ON crm_sequence_enrollment (contact_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_sequence_enrollment_deal_id ON crm_sequence_enrollment (deal_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_sequence_enrollment_enrolled_by_user_id ON crm_sequence_enrollment (enrolled_by_user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_sequence_step (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
sequence_id INTEGER NOT NULL,
|
||||
step_number INTEGER NOT NULL,
|
||||
step_type INTEGER NOT NULL,
|
||||
delay_days INTEGER DEFAULT 0 NOT NULL,
|
||||
delay_hours INTEGER DEFAULT 0,
|
||||
email_template_id INTEGER,
|
||||
task_subject VARCHAR(500),
|
||||
task_description TEXT,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT uq_crm_sequence_step_1 UNIQUE (sequence_id, step_number)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_sequence_step_sequence_id ON crm_sequence_step (sequence_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_sequence_step_email_template_id ON crm_sequence_step (email_template_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_win_loss_reason (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
type INTEGER NOT NULL,
|
||||
sort_order INTEGER NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
create_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
modify_date TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT uq_crm_win_loss_reason_1 UNIQUE (name)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_crm_consent_record_evidence_form_submission_id ON crm_consent_record (evidence_form_submission_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crm_job_state (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(250) NOT NULL UNIQUE,
|
||||
last_success TIMESTAMP WITHOUT TIME ZONE,
|
||||
last_full_pass TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
@@ -1,295 +0,0 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
set -euf
|
||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||
|
||||
# OS specific support.
|
||||
native_path() { printf %s\\n "$1"; }
|
||||
case "$(uname)" in
|
||||
CYGWIN* | MINGW*)
|
||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||
native_path() { cygpath --path --windows "$1"; }
|
||||
;;
|
||||
esac
|
||||
|
||||
# set JAVACMD and JAVACCMD
|
||||
set_java_home() {
|
||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||
if [ -n "${JAVA_HOME-}" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||
|
||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v java
|
||||
)" || :
|
||||
JAVACCMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v javac
|
||||
)" || :
|
||||
|
||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# hash string like Java String::hashCode
|
||||
hash_string() {
|
||||
str="${1:-}" h=0
|
||||
while [ -n "$str" ]; do
|
||||
char="${str%"${str#?}"}"
|
||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||
str="${str#?}"
|
||||
done
|
||||
printf %x\\n $h
|
||||
}
|
||||
|
||||
verbose() { :; }
|
||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||
|
||||
die() {
|
||||
printf %s\\n "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
# MWRAPPER-139:
|
||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||
# Needed for removing poorly interpreted newline sequences when running in more
|
||||
# exotic environments such as mingw bash on Windows.
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
scriptDir="$(dirname "$0")"
|
||||
scriptName="$(basename "$0")"
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||
*)
|
||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||
distributionPlatform=linux-amd64
|
||||
;;
|
||||
esac
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
actualDistributionDir=""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
|
||||
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$distributionUrlNameMain"
|
||||
fi
|
||||
fi
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
# enable globbing to iterate over items
|
||||
set +f
|
||||
for dir in "$TMP_DOWNLOAD_DIR"/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
if [ -f "$dir/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$(basename "$dir")"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
set -f
|
||||
fi
|
||||
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
verbose "Contents of $TMP_DOWNLOAD_DIR:"
|
||||
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
|
||||
die "Could not find Maven distribution directory in extracted archive"
|
||||
fi
|
||||
|
||||
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
||||
@@ -1,189 +0,0 @@
|
||||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
|
||||
$MAVEN_M2_PATH = "$HOME/.m2"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
|
||||
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
|
||||
}
|
||||
|
||||
$MAVEN_WRAPPER_DISTS = $null
|
||||
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
|
||||
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
|
||||
} else {
|
||||
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
|
||||
}
|
||||
|
||||
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
$actualDistributionDir = ""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
|
||||
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
|
||||
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
|
||||
$actualDistributionDir = $distributionUrlNameMain
|
||||
}
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if (!$actualDistributionDir) {
|
||||
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
|
||||
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
|
||||
if (Test-Path -Path $testPath -PathType Leaf) {
|
||||
$actualDistributionDir = $_.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$actualDistributionDir) {
|
||||
Write-Error "Could not find Maven distribution directory in extracted archive"
|
||||
}
|
||||
|
||||
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
@@ -22,18 +22,13 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<properties>
|
||||
<revision>0.2.0-SNAPSHOT</revision>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<jacoco.version>0.8.15</jacoco.version>
|
||||
<qbit.guardrails.enforce>true</qbit.guardrails.enforce>
|
||||
<qbit.guardrails.strictUiApiRoutes>true</qbit.guardrails.strictUiApiRoutes>
|
||||
<revision>0.1.0</revision>
|
||||
</properties>
|
||||
|
||||
<parent>
|
||||
<groupId>com.kingsrook</groupId>
|
||||
<artifactId>qbit-build-parent</artifactId>
|
||||
<version>2.1.2</version>
|
||||
<version>1.4.0</version>
|
||||
</parent>
|
||||
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
@@ -73,28 +68,6 @@
|
||||
</scm>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-middleware-api</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-middleware-health</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
<artifactId>qbit-email</artifactId>
|
||||
<version>0.1.0-RC.3</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-middleware-javalin</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-core</artifactId>
|
||||
@@ -109,14 +82,7 @@
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<version>2.3.232</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.liquibase</groupId>
|
||||
<artifactId>liquibase-core</artifactId>
|
||||
<version>4.33.0</version>
|
||||
<version>2.2.220</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -129,129 +95,9 @@
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<version>3.27.7</version>
|
||||
<version>3.27.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<resources>
|
||||
<!-- help bundles carry the artifact version (QQQ help bundle provenance) -->
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
<includes>
|
||||
<include>help/*.yaml</include>
|
||||
<include>qbit-crm.properties</include>
|
||||
</includes>
|
||||
</resource>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>false</filtering>
|
||||
<excludes>
|
||||
<exclude>help/*.yaml</exclude>
|
||||
<exclude>qbit-crm.properties</exclude>
|
||||
</excludes>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<systemPropertyVariables combine.children="append">
|
||||
<qbit.artifactVersion>${project.version}</qbit.artifactVersion>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<configuration>
|
||||
<configLocation>checkstyle/config.xml</configLocation>
|
||||
<includeTestSourceDirectory>true</includeTestSourceDirectory>
|
||||
<violationSeverity>warning</violationSeverity>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>checkstyle</id>
|
||||
<phase>validate</phase>
|
||||
<goals><goal>check</goal></goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>${jacoco.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare-agent</id>
|
||||
<goals>
|
||||
<goal>prepare-agent</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>report</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>report</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>check</id>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<rules>
|
||||
<rule>
|
||||
<element>BUNDLE</element>
|
||||
<limits>
|
||||
<limit>
|
||||
<counter>INSTRUCTION</counter>
|
||||
<value>COVEREDRATIO</value>
|
||||
<minimum>0.70</minimum>
|
||||
</limit>
|
||||
<limit>
|
||||
<counter>CLASS</counter>
|
||||
<value>COVEREDRATIO</value>
|
||||
<minimum>0.90</minimum>
|
||||
</limit>
|
||||
</limits>
|
||||
</rule>
|
||||
</rules>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<!-- Built against qqq 4.1 (QBitMetaDataProducer). Until qbit-build-parent -->
|
||||
<!-- imports the released 4.1 BOM, this profile is active by default and -->
|
||||
<!-- imports qqq-bom-pom 4.1.0-RC.7 ahead of the parent's 4.0.0 BOM. -->
|
||||
<profile>
|
||||
<id>qqq-snapshot</id>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<properties>
|
||||
<qqq.snapshot.version>4.1.0-RC.7</qqq.snapshot.version>
|
||||
</properties>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-bom-pom</artifactId>
|
||||
<version>${qqq.snapshot.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
@@ -1,84 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package com.kingsrook.qbits.crm;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.crm.security.CrmPermissions;
|
||||
import com.kingsrook.qbits.crm.widgets.CrmHomeWidgets;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The CRM home and the two navigation sections. Child tables stay on records.
|
||||
*******************************************************************************/
|
||||
public final class CrmAppMetaData
|
||||
{
|
||||
public static final List<String> MAIN_TABLES = List.of("crm_contact", "crm_company", "crm_deal", "crm_activity", "crm_form_submission", "crm_email_list");
|
||||
public static final List<String> SETTINGS_TABLES = List.of("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");
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Metadata factory only.
|
||||
*******************************************************************************/
|
||||
private CrmAppMetaData()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Main navigation, also available to hosts embedding these tables.
|
||||
*******************************************************************************/
|
||||
public static QAppSection mainSection()
|
||||
{
|
||||
return (new QAppSection().withName("crm").withLabel("CRM").withIcon(new QIcon("contacts"))
|
||||
.withTables(new ArrayList<>(MAIN_TABLES)));
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Construct the app after package discovery has produced the tables.
|
||||
*******************************************************************************/
|
||||
public static QAppMetaData build(MetaDataProducerMultiOutput output, String source)
|
||||
{
|
||||
QAppMetaData app = new QAppMetaData().withName("crm").withLabel("CRM").withIcon(new QIcon("contacts"))
|
||||
.withPermissionRules(CrmPermissions.hasAccessRules(CrmPermissions.BASE))
|
||||
.withWidgets(new ArrayList<>(CrmHomeWidgets.HOME))
|
||||
.withSection(mainSection())
|
||||
.withSection(new QAppSection().withName("crm_settings").withLabel("Settings").withIcon(new QIcon("settings"))
|
||||
.withTables(new ArrayList<>(SETTINGS_TABLES)));
|
||||
List<String> tables = new ArrayList<>(MAIN_TABLES);
|
||||
tables.addAll(SETTINGS_TABLES);
|
||||
for(String name : tables)
|
||||
{
|
||||
app.withChild(output.get(QTableMetaData.class, name));
|
||||
}
|
||||
return (app);
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.crm;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.crm.security.CrmDataPolicy;
|
||||
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData;
|
||||
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Optional host API publication and the core record-search contract.
|
||||
*******************************************************************************/
|
||||
public final class CrmIntegrations
|
||||
{
|
||||
private static final Map<String, List<String>> SEARCH_FIELDS = Map.of(
|
||||
"crm_contact", List.of("firstName", "lastName", "email"),
|
||||
"crm_company", List.of("name", "domain"), "crm_deal", List.of("name"),
|
||||
"crm_activity", List.of("subject"), "crm_form_submission", List.of("email"), "crm_email_list", List.of("name"));
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private CrmIntegrations()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Enable publication only when the host supplies both API settings.
|
||||
*******************************************************************************/
|
||||
public static void apply(MetaDataProducerMultiOutput output, CrmQBitConfig config) throws QException
|
||||
{
|
||||
if(config.getEsbDestinationName() != null)
|
||||
{
|
||||
throw (new QException("CRM ESB publication requires classified-value exclusion, which QQQ 4.1.0-RC.6 does not provide."));
|
||||
}
|
||||
boolean api = config.getApiName() != null && config.getApiVersion() != null;
|
||||
if(api)
|
||||
{
|
||||
try
|
||||
{
|
||||
Class.forName("com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer");
|
||||
}
|
||||
catch(ClassNotFoundException | LinkageError e)
|
||||
{
|
||||
throw (new QException("CRM API publication requires qqq-middleware-api on the host classpath.", e));
|
||||
}
|
||||
}
|
||||
for(QTableMetaData table : output.getEach(QTableMetaData.class))
|
||||
{
|
||||
if(SEARCH_FIELDS.containsKey(table.getName()))
|
||||
{
|
||||
table.withSearchFields(SEARCH_FIELDS.get(table.getName()));
|
||||
}
|
||||
if(api && CrmDataPolicy.isPublishedTable(table.getName()))
|
||||
{
|
||||
ApiTableMetaDataContainer.ofOrWithNew(table).withApiTableMetaData(config.getApiName(),
|
||||
new ApiTableMetaData().withInitialVersion(config.getApiVersion()).withIsExcluded(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package com.kingsrook.qbits.crm;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ChildRecordListRenderer;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** CRM presentation shared by package-discovered tables and child widgets.
|
||||
*******************************************************************************/
|
||||
public final class CrmPresentation
|
||||
{
|
||||
private static final Map<String, String> LABELS = Map.ofEntries(
|
||||
Map.entry("crm_contact", "Contacts"),
|
||||
Map.entry("crm_company", "Companies"),
|
||||
Map.entry("crm_deal", "Deals"),
|
||||
Map.entry("crm_activity", "Activities"),
|
||||
Map.entry("crm_form_submission", "Web Leads"),
|
||||
Map.entry("crm_email_list", "Lists"),
|
||||
Map.entry("crm_pipeline", "Pipelines"),
|
||||
Map.entry("crm_pipeline_stage", "Pipeline Stages"),
|
||||
Map.entry("crm_product", "Products"),
|
||||
Map.entry("crm_email_template", "Email Templates"),
|
||||
Map.entry("crm_email_sequence", "Email Sequences"),
|
||||
Map.entry("crm_sequence_step", "Sequence Steps"),
|
||||
Map.entry("crm_sequence_enrollment", "Sequence Enrollments"),
|
||||
Map.entry("crm_lead_score_rule", "Lead Score Rules"),
|
||||
Map.entry("crm_assignment_rule", "Assignment Rules"),
|
||||
Map.entry("crm_assignment_rule_member", "Assignment Rule Members"),
|
||||
Map.entry("crm_sales_goal", "Sales Goals"),
|
||||
Map.entry("crm_currency", "Currencies"),
|
||||
Map.entry("crm_lifecycle_stage", "Lifecycle Stages"),
|
||||
Map.entry("crm_lead_source", "Lead Sources"),
|
||||
Map.entry("crm_industry", "Industries"),
|
||||
Map.entry("crm_activity_type", "Activity Types"),
|
||||
Map.entry("crm_activity_outcome", "Activity Outcomes"),
|
||||
Map.entry("crm_contact_role", "Contact Roles"),
|
||||
Map.entry("crm_win_loss_reason", "Win/Loss Reasons"),
|
||||
Map.entry("crm_consent_record", "Consent Records"),
|
||||
Map.entry("crm_email_list_member", "List Members"),
|
||||
Map.entry("crm_email_message", "Email Messages"),
|
||||
Map.entry("crm_deal_contact", "Deal Contacts"),
|
||||
Map.entry("crm_deal_product", "Deal Products"),
|
||||
Map.entry("crm_deal_stage_history", "Deal Stage History"),
|
||||
Map.entry("crm_activity_participant", "Activity Participants"),
|
||||
Map.entry("crm_external_mapping", "External Mappings"),
|
||||
Map.entry("crm_job_state", "Job State"));
|
||||
private static final Map<String, List<String>> CHILD_FIELDS = Map.ofEntries(
|
||||
Map.entry("crm_contact", List.of("firstName", "lastName", "email", "phone")),
|
||||
Map.entry("crm_deal", List.of("name", "pipelineStageId", "amount", "expectedCloseDate")),
|
||||
Map.entry("crm_activity", List.of("subject", "activityTypeId", "startDate")),
|
||||
Map.entry("crm_form_submission", List.of("formKey", "createDate", "conversionStatus")),
|
||||
Map.entry("crm_email_list_member", List.of("emailListId", "status")),
|
||||
Map.entry("crm_consent_record", List.of("consentType", "status", "consentDate")),
|
||||
Map.entry("crm_sequence_enrollment", List.of("sequenceId", "status", "enrolledDate")),
|
||||
Map.entry("crm_email_message", List.of("subject", "status", "sentDate")),
|
||||
Map.entry("crm_deal_contact", List.of("contactId", "contactRoleId", "isPrimary")),
|
||||
Map.entry("crm_deal_product", List.of("productId", "quantity", "unitPrice", "totalAmount")),
|
||||
Map.entry("crm_deal_stage_history", List.of("fromStageId", "toStageId", "transitionDate", "userId")));
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private CrmPresentation()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Explicit labels avoid exposing technical table and user-id names.
|
||||
*******************************************************************************/
|
||||
public static void configureTable(QTableMetaData table)
|
||||
{
|
||||
table.setLabel(LABELS.get(table.getName()));
|
||||
Map<String, String> users = Map.of("ownerUserId", "Owner", "assignedToUserId", "Assigned To",
|
||||
"assignToUserId", "Assign To", "enrolledByUserId", "Enrolled By", "convertedByUserId", "Converted By", "userId", "User");
|
||||
table.getFields().values().forEach(field ->
|
||||
{
|
||||
if(users.containsKey(field.getName()))
|
||||
{
|
||||
field.setLabel(users.get(field.getName()));
|
||||
}
|
||||
});
|
||||
Map<String, List<String>> money = Map.of("crm_deal", List.of("amount", "amountInBaseCurrency", "weightedAmount"),
|
||||
"crm_deal_product", List.of("unitPrice", "totalAmount"), "crm_product", List.of("unitPrice"), "crm_sales_goal", List.of("targetAmount"));
|
||||
for(String field : money.getOrDefault(table.getName(), List.of()))
|
||||
{
|
||||
table.getField(field).setDisplayFormat(DisplayFormat.CURRENCY);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** RC.6 supports a per-widget field list, preserving each full child table.
|
||||
*******************************************************************************/
|
||||
public static void configureChildList(QWidgetMetaData widget, String childTable)
|
||||
{
|
||||
List<String> fields = CHILD_FIELDS.get(childTable);
|
||||
if(fields != null)
|
||||
{
|
||||
widget.withDefaultValue(ChildRecordListRenderer.KEY_ONLY_INCLUDE_FIELD_NAMES, new ArrayList<>(fields));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +1,36 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Configuration for the CRM QBit.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitConfig;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QBitConfig for the CRM QBit. Holds backend name, security configuration,
|
||||
** table metadata customizer and backend column naming.
|
||||
** table metadata customizer, and companion QBit namespace references.
|
||||
*******************************************************************************/
|
||||
public class CrmQBitConfig implements QBitConfig
|
||||
{
|
||||
public static final CrmQBitConfig DEFAULT_CONFIG = new CrmQBitConfig();
|
||||
|
||||
private String tenantSecurityKeyType;
|
||||
private String tenantAllAccessKeyName;
|
||||
private String apiName;
|
||||
private String apiVersion;
|
||||
private String esbDestinationName;
|
||||
|
||||
private String schedulerName;
|
||||
private Boolean schedulesEnabled = true;
|
||||
|
||||
private String messagingProviderName;
|
||||
|
||||
private String publicBaseUrl;
|
||||
|
||||
private String tokenSecret;
|
||||
|
||||
private String defaultBackendNameForTables;
|
||||
private Boolean inferBackendNames = true;
|
||||
private Boolean appEnabled = true;
|
||||
private String tableNamePrefix;
|
||||
private MetaDataCustomizerInterface<QTableMetaData> tableMetaDataCustomizer;
|
||||
private List<QFieldMetaData> securityFields;
|
||||
private List<RecordSecurityLock> recordSecurityLocks;
|
||||
|
||||
private String quickSearchQBitNamespace;
|
||||
private String webhooksQBitNamespace;
|
||||
private String workflowsQBitNamespace;
|
||||
|
||||
|
||||
|
||||
@@ -140,227 +111,127 @@ public class CrmQBitConfig implements QBitConfig
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether to infer snake_case database column names.
|
||||
** Getter for tableNamePrefix
|
||||
*******************************************************************************/
|
||||
public Boolean getInferBackendNames()
|
||||
public String getTableNamePrefix()
|
||||
{
|
||||
return (this.inferBackendNames);
|
||||
return (this.tableNamePrefix);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Configure automatic database column naming.
|
||||
** Fluent setter for tableNamePrefix
|
||||
*******************************************************************************/
|
||||
public CrmQBitConfig withInferBackendNames(Boolean inferBackendNames)
|
||||
public CrmQBitConfig withTableNamePrefix(String tableNamePrefix)
|
||||
{
|
||||
this.inferBackendNames = inferBackendNames;
|
||||
this.tableNamePrefix = tableNamePrefix;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether to publish the standalone CRM app.
|
||||
** Getter for securityFields
|
||||
*******************************************************************************/
|
||||
public Boolean getAppEnabled()
|
||||
public List<QFieldMetaData> getSecurityFields()
|
||||
{
|
||||
return (appEnabled);
|
||||
return (this.securityFields);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Allow a host to put CRM tables in its own app.
|
||||
** Fluent setter for securityFields
|
||||
*******************************************************************************/
|
||||
public CrmQBitConfig withAppEnabled(Boolean appEnabled)
|
||||
public CrmQBitConfig withSecurityFields(List<QFieldMetaData> securityFields)
|
||||
{
|
||||
this.appEnabled = appEnabled;
|
||||
this.securityFields = securityFields;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for tokenSecret
|
||||
** Getter for recordSecurityLocks
|
||||
*******************************************************************************/
|
||||
public String getTokenSecret()
|
||||
public List<RecordSecurityLock> getRecordSecurityLocks()
|
||||
{
|
||||
return (tokenSecret);
|
||||
return (this.recordSecurityLocks);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Configure tokenSecret
|
||||
** Fluent setter for recordSecurityLocks
|
||||
*******************************************************************************/
|
||||
public CrmQBitConfig withTokenSecret(String tokenSecret)
|
||||
public CrmQBitConfig withRecordSecurityLocks(List<RecordSecurityLock> recordSecurityLocks)
|
||||
{
|
||||
this.tokenSecret = tokenSecret;
|
||||
this.recordSecurityLocks = recordSecurityLocks;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for publicBaseUrl
|
||||
** Getter for quickSearchQBitNamespace
|
||||
*******************************************************************************/
|
||||
public String getPublicBaseUrl()
|
||||
public String getQuickSearchQBitNamespace()
|
||||
{
|
||||
return (publicBaseUrl);
|
||||
return (this.quickSearchQBitNamespace);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Configure publicBaseUrl
|
||||
** Fluent setter for quickSearchQBitNamespace
|
||||
*******************************************************************************/
|
||||
public CrmQBitConfig withPublicBaseUrl(String publicBaseUrl)
|
||||
public CrmQBitConfig withQuickSearchQBitNamespace(String quickSearchQBitNamespace)
|
||||
{
|
||||
this.publicBaseUrl = publicBaseUrl;
|
||||
this.quickSearchQBitNamespace = quickSearchQBitNamespace;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for messagingProviderName
|
||||
** Getter for webhooksQBitNamespace
|
||||
*******************************************************************************/
|
||||
public String getMessagingProviderName()
|
||||
public String getWebhooksQBitNamespace()
|
||||
{
|
||||
return (messagingProviderName);
|
||||
return (this.webhooksQBitNamespace);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Configure messagingProviderName
|
||||
*******************************************************************************/
|
||||
public CrmQBitConfig withMessagingProviderName(String messagingProviderName)
|
||||
{
|
||||
this.messagingProviderName = messagingProviderName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Scheduler configuration: schedulerName.
|
||||
** Fluent setter for webhooksQBitNamespace
|
||||
*******************************************************************************/
|
||||
public String getSchedulerName()
|
||||
public CrmQBitConfig withWebhooksQBitNamespace(String webhooksQBitNamespace)
|
||||
{
|
||||
return (schedulerName);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Configure schedulerName.
|
||||
*******************************************************************************/
|
||||
public CrmQBitConfig withSchedulerName(String schedulerName)
|
||||
{
|
||||
this.schedulerName = schedulerName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Scheduler configuration: schedulesEnabled.
|
||||
*******************************************************************************/
|
||||
public Boolean getSchedulesEnabled()
|
||||
{
|
||||
return (schedulesEnabled);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Configure schedulesEnabled.
|
||||
*******************************************************************************/
|
||||
public CrmQBitConfig withSchedulesEnabled(Boolean schedulesEnabled)
|
||||
{
|
||||
this.schedulesEnabled = schedulesEnabled;
|
||||
return (this);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for tenantSecurityKeyType.
|
||||
*******************************************************************************/
|
||||
public String getTenantSecurityKeyType()
|
||||
{
|
||||
return (tenantSecurityKeyType);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Configure tenantSecurityKeyType; null disables the optional integration.
|
||||
*******************************************************************************/
|
||||
public CrmQBitConfig withTenantSecurityKeyType(String tenantSecurityKeyType)
|
||||
{
|
||||
this.tenantSecurityKeyType = tenantSecurityKeyType;
|
||||
this.webhooksQBitNamespace = webhooksQBitNamespace;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for tenantAllAccessKeyName.
|
||||
** Getter for workflowsQBitNamespace
|
||||
*******************************************************************************/
|
||||
public String getTenantAllAccessKeyName()
|
||||
public String getWorkflowsQBitNamespace()
|
||||
{
|
||||
return (tenantAllAccessKeyName);
|
||||
return (this.workflowsQBitNamespace);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Configure tenantAllAccessKeyName; null disables the optional integration.
|
||||
** Fluent setter for workflowsQBitNamespace
|
||||
*******************************************************************************/
|
||||
public CrmQBitConfig withTenantAllAccessKeyName(String tenantAllAccessKeyName)
|
||||
public CrmQBitConfig withWorkflowsQBitNamespace(String workflowsQBitNamespace)
|
||||
{
|
||||
this.tenantAllAccessKeyName = tenantAllAccessKeyName;
|
||||
this.workflowsQBitNamespace = workflowsQBitNamespace;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for apiName.
|
||||
*******************************************************************************/
|
||||
public String getApiName()
|
||||
{
|
||||
return (apiName);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Configure apiName; null disables the optional integration.
|
||||
*******************************************************************************/
|
||||
public CrmQBitConfig withApiName(String apiName)
|
||||
{
|
||||
this.apiName = apiName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for apiVersion.
|
||||
*******************************************************************************/
|
||||
public String getApiVersion()
|
||||
{
|
||||
return (apiVersion);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Configure apiVersion; null disables the optional integration.
|
||||
*******************************************************************************/
|
||||
public CrmQBitConfig withApiVersion(String apiVersion)
|
||||
{
|
||||
this.apiVersion = apiVersion;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for esbDestinationName.
|
||||
*******************************************************************************/
|
||||
public String getEsbDestinationName()
|
||||
{
|
||||
return (esbDestinationName);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Configure esbDestinationName; null disables the optional integration.
|
||||
*******************************************************************************/
|
||||
public CrmQBitConfig withEsbDestinationName(String esbDestinationName)
|
||||
{
|
||||
this.esbDestinationName = esbDestinationName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,84 +1,34 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QBitMetaDataProducer for the CRM QBit.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import com.kingsrook.qbits.crm.activities.customizers.ActivityLastDateCustomizer;
|
||||
import com.kingsrook.qbits.crm.activities.model.Activity;
|
||||
import com.kingsrook.qbits.crm.core.customizers.CurrencyRateDateCustomizer;
|
||||
import com.kingsrook.qbits.crm.core.customizers.FormSubmissionAutoLinkCustomizer;
|
||||
import com.kingsrook.qbits.crm.core.model.FormSubmission;
|
||||
import com.kingsrook.qbits.crm.audit.customizers.CrmAuditLogCustomizer;
|
||||
import com.kingsrook.qbits.crm.core.model.Company;
|
||||
import com.kingsrook.qbits.crm.core.model.Contact;
|
||||
import com.kingsrook.qbits.crm.core.model.Currency;
|
||||
import com.kingsrook.qbits.crm.core.model.FormSubmission;
|
||||
import com.kingsrook.qbits.crm.deals.customizers.DealInitializationCustomizer;
|
||||
import com.kingsrook.qbits.crm.deals.customizers.DealProductRecalculationCustomizer;
|
||||
import com.kingsrook.qbits.crm.deals.customizers.DealStageChangeCustomizer;
|
||||
import com.kingsrook.qbits.crm.deals.model.Deal;
|
||||
import com.kingsrook.qbits.crm.deals.model.DealProduct;
|
||||
import com.kingsrook.qbits.crm.email.CrmEmailProcessIdentity;
|
||||
import com.kingsrook.qbits.crm.scoring.AutoAssignCustomizer;
|
||||
import com.kingsrook.qbits.crm.security.CrmDataPolicy;
|
||||
import com.kingsrook.qbits.crm.security.CrmPermissions;
|
||||
import com.kingsrook.qbits.crm.security.CrmRelationshipValidator;
|
||||
import com.kingsrook.qbits.crm.widgets.CrmWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.MultiCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.alerts.QAlertTypeMetaData;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceWithProperties;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.collaboration.RecordFeature;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.collaboration.RecordFeaturesMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.collaboration.RecordOwnership;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.bundles.HelpBundles;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaDataProducer;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -89,40 +39,12 @@ public class CrmQBitProducer implements QBitMetaDataProducer<CrmQBitConfig>
|
||||
{
|
||||
public static final String GROUP_ID = "com.kingsrook.qbits";
|
||||
public static final String ARTIFACT_ID = "crm";
|
||||
public static final String VERSION = loadVersion();
|
||||
public static final String VERSION = "0.1.0";
|
||||
|
||||
private CrmQBitConfig qBitConfig;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Read the artifact version from the Maven-filtered properties resource.
|
||||
*******************************************************************************/
|
||||
private static String loadVersion()
|
||||
{
|
||||
try(InputStream inputStream = CrmQBitProducer.class.getClassLoader().getResourceAsStream("qbit-crm.properties"))
|
||||
{
|
||||
if(inputStream == null)
|
||||
{
|
||||
throw new IllegalStateException("Missing qbit-crm.properties; build with Maven to provide the artifact version");
|
||||
}
|
||||
Properties properties = new Properties();
|
||||
properties.load(inputStream);
|
||||
String version = properties.getProperty("version");
|
||||
if(version == null || version.isBlank() || version.contains("${"))
|
||||
{
|
||||
throw new IllegalStateException("Missing or unfiltered version in qbit-crm.properties; build with Maven to provide the artifact version");
|
||||
}
|
||||
return (version);
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
throw new IllegalStateException("Could not read artifact version from qbit-crm.properties", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Produce the QBitMetaData for the CRM QBit.
|
||||
*******************************************************************************/
|
||||
@@ -145,84 +67,12 @@ public class CrmQBitProducer implements QBitMetaDataProducer<CrmQBitConfig>
|
||||
@Override
|
||||
public void postProduceActions(MetaDataProducerMultiOutput metaDataProducerMultiOutput, QInstance qInstance) throws QException
|
||||
{
|
||||
for(QJoinMetaData join : metaDataProducerMultiOutput.getEach(QJoinMetaData.class))
|
||||
{
|
||||
join.setName(snakeCase(join.getName()));
|
||||
}
|
||||
CrmDataPolicy.apply(metaDataProducerMultiOutput, qInstance, qBitConfig);
|
||||
CrmIntegrations.apply(metaDataProducerMultiOutput, qBitConfig);
|
||||
for(QWidgetMetaData widget : metaDataProducerMultiOutput.getEach(QWidgetMetaData.class))
|
||||
{
|
||||
widget.setName(snakeCase(widget.getName()));
|
||||
QCodeReference permissionChecker = widget.getPermissionRules() == null ? null : widget.getPermissionRules().getCustomPermissionChecker();
|
||||
widget.setPermissionRules(CrmPermissions.hasAccessRules(CrmPermissions.BASE).withCustomPermissionChecker(permissionChecker));
|
||||
widget.setGridColumns(com.kingsrook.qbits.crm.widgets.CrmHomeWidgets.TODAY.contains(widget.getName()) ? 4 : 12);
|
||||
if(widget.getDefaultValues() != null && widget.getDefaultValues().get("joinName") instanceof String joinName)
|
||||
{
|
||||
String normalizedJoinName = snakeCase(joinName);
|
||||
widget.withDefaultValue("joinName", normalizedJoinName);
|
||||
QJoinMetaData join = metaDataProducerMultiOutput.getEach(QJoinMetaData.class).stream()
|
||||
.filter(candidate -> candidate.getName().equals(normalizedJoinName)).findFirst().orElseThrow();
|
||||
CrmPresentation.configureChildList(widget, join.getRightTable());
|
||||
QTableMetaData parent = metaDataProducerMultiOutput.get(QTableMetaData.class, join.getLeftTable());
|
||||
boolean alreadyAttached = parent.getSections() != null && parent.getSections().stream()
|
||||
.anyMatch(section -> section.getWidgetName() != null && snakeCase(section.getWidgetName()).equals(widget.getName()));
|
||||
if(!alreadyAttached)
|
||||
{
|
||||
parent.withSection(new QFieldSection(widget.getName(), widget.getLabel(), new QIcon("list"), Tier.T2, List.of())
|
||||
.withWidgetName(widget.getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
metaDataProducerMultiOutput.get(QTableMetaData.class, Contact.TABLE_NAME).withSection(
|
||||
new QFieldSection("crm_contact_timeline", "Timeline", new QIcon("history"), Tier.T2, List.of())
|
||||
.withWidgetName("crm_contact_timeline"));
|
||||
|
||||
for(QProcessMetaData process : metaDataProducerMultiOutput.getEach(QProcessMetaData.class))
|
||||
{
|
||||
process.setPermissionRules(CrmPermissions.processRules(process.getName()));
|
||||
process.setSourceQBitName(getQBitMetaData().getName());
|
||||
Integer seconds = Map.of("crm_process_sequence_steps", 300, "crm_calculate_lead_scores", 3600,
|
||||
"crm_sync_email_delivery", 900, "crm_expire_consents", 86400).get(process.getName());
|
||||
if(seconds != null && qBitConfig.getSchedulerName() != null && !Boolean.FALSE.equals(qBitConfig.getSchedulesEnabled()))
|
||||
{
|
||||
process.withSchedule(new QScheduleMetaData().withSchedulerName(qBitConfig.getSchedulerName()).withRepeatSeconds(seconds));
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// iterate tables produced by this QBit and register customizers //
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
for(QTableMetaData table : metaDataProducerMultiOutput.getEach(QTableMetaData.class))
|
||||
{
|
||||
String tableName = table.getName();
|
||||
CrmPresentation.configureTable(table);
|
||||
if(tableName.equals(Contact.TABLE_NAME))
|
||||
{
|
||||
addOrComposeCustomizer(table, TableCustomizers.POST_QUERY_RECORD, new QCodeReference(com.kingsrook.qbits.crm.core.customizers.ContactLabelCustomizer.class));
|
||||
}
|
||||
if(tableName.equals(Deal.TABLE_NAME) || tableName.equals(Activity.TABLE_NAME))
|
||||
{
|
||||
addOrComposeCustomizer(table, TableCustomizers.PRE_INSERT_RECORD, new QCodeReference(CrmRelationshipValidator.class));
|
||||
addOrComposeCustomizer(table, TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(CrmRelationshipValidator.class));
|
||||
}
|
||||
table.setPermissionRules(CrmPermissions.tableRules(tableName));
|
||||
table.setSourceQBitName(getQBitMetaData().getName());
|
||||
if(table.getSections() != null)
|
||||
{
|
||||
table.getSections().forEach(section ->
|
||||
{
|
||||
if(section.getWidgetName() != null)
|
||||
{
|
||||
section.setWidgetName(snakeCase(section.getWidgetName()));
|
||||
}
|
||||
});
|
||||
}
|
||||
if(Boolean.TRUE.equals(qBitConfig.getInferBackendNames()))
|
||||
{
|
||||
QInstanceEnricher.setInferredFieldBackendNames(table);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////
|
||||
// wire audit log on tracked entities //
|
||||
@@ -230,27 +80,15 @@ public class CrmQBitProducer implements QBitMetaDataProducer<CrmQBitConfig>
|
||||
if(tableName.equals(Contact.TABLE_NAME) || tableName.equals(Company.TABLE_NAME)
|
||||
|| tableName.equals(Deal.TABLE_NAME) || tableName.equals(Activity.TABLE_NAME))
|
||||
{
|
||||
table.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.FIELD));
|
||||
RecordFeaturesMetaData features = RecordFeaturesMetaData.ofOrWithNew(table)
|
||||
.withOwnership(new RecordOwnership().withOwnerFieldName("ownerUserId"));
|
||||
for(RecordFeature feature : List.of(RecordFeature.TAGS, RecordFeature.COMMENTS, RecordFeature.ATTACHMENTS,
|
||||
RecordFeature.FOLLOW, RecordFeature.TIMELINE, RecordFeature.RECENTS, RecordFeature.FAVORITES))
|
||||
{
|
||||
features.withEnabled(feature, true);
|
||||
}
|
||||
if(!tableName.equals(Activity.TABLE_NAME))
|
||||
{
|
||||
features.withEnabled(RecordFeature.TASKS, true).withEnabled(RecordFeature.ASSIGNMENT, true);
|
||||
}
|
||||
QCodeReference auditRef = new QCodeReference(CrmAuditLogCustomizer.class);
|
||||
addOrComposeCustomizer(table, TableCustomizers.POST_INSERT_RECORD, auditRef);
|
||||
addOrComposeCustomizer(table, TableCustomizers.POST_UPDATE_RECORD, auditRef);
|
||||
addOrComposeCustomizer(table, TableCustomizers.POST_DELETE_RECORD, auditRef);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////
|
||||
// wire activity timestamp propagation //
|
||||
///////////////////////////////////////////////////////////////
|
||||
if(tableName.equals(Contact.TABLE_NAME) || tableName.equals(Deal.TABLE_NAME))
|
||||
{
|
||||
addOrComposeCustomizer(table, TableCustomizers.POST_INSERT_RECORD, new QCodeReference(AutoAssignCustomizer.class));
|
||||
}
|
||||
if(tableName.equals(Activity.TABLE_NAME))
|
||||
{
|
||||
addOrComposeCustomizer(table, TableCustomizers.POST_INSERT_RECORD, new QCodeReference(ActivityLastDateCustomizer.class));
|
||||
@@ -265,15 +103,6 @@ public class CrmQBitProducer implements QBitMetaDataProducer<CrmQBitConfig>
|
||||
addOrComposeCustomizer(table, TableCustomizers.POST_INSERT_RECORD, new QCodeReference(DealInitializationCustomizer.class));
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////
|
||||
// stamp the exchange-rate date when a rate is set or changed //
|
||||
///////////////////////////////////////////////////////////////
|
||||
if(tableName.equals(Currency.TABLE_NAME))
|
||||
{
|
||||
addOrComposeCustomizer(table, TableCustomizers.PRE_INSERT_RECORD, new QCodeReference(CurrencyRateDateCustomizer.OnInsert.class));
|
||||
addOrComposeCustomizer(table, TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(CurrencyRateDateCustomizer.OnUpdate.class));
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////
|
||||
// wire deal product recalculation //
|
||||
///////////////////////////////////////////////////////////////
|
||||
@@ -342,7 +171,10 @@ public class CrmQBitProducer implements QBitMetaDataProducer<CrmQBitConfig>
|
||||
*******************************************************************************/
|
||||
public static QAppSection produceAppSection()
|
||||
{
|
||||
return (CrmAppMetaData.mainSection());
|
||||
return new QAppSection()
|
||||
.withName("crm")
|
||||
.withLabel("CRM")
|
||||
.withIcon(new QIcon().withName("contacts"));
|
||||
}
|
||||
|
||||
|
||||
@@ -377,63 +209,4 @@ public class CrmQBitProducer implements QBitMetaDataProducer<CrmQBitConfig>
|
||||
return (this);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Normalize generated metadata names as well as explicit CRM names.
|
||||
*******************************************************************************/
|
||||
private static String snakeCase(String name)
|
||||
{
|
||||
return (name.replaceAll("([a-z0-9])([A-Z])", "$1_$2").toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Add widget provenance after the standard package-discovered production.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public MetaDataProducerMultiOutput produce(QInstance qInstance) throws QException
|
||||
{
|
||||
MetaDataProducerMultiOutput produced = QBitMetaDataProducer.super.produce(qInstance);
|
||||
MetaDataProducerMultiOutput output = new MetaDataProducerMultiOutput();
|
||||
for(MetaDataProducerOutput item : produced.getEach(MetaDataProducerOutput.class))
|
||||
{
|
||||
output.add(item instanceof QWidgetMetaData widget
|
||||
? new CrmWidgetMetaData(widget).withSourceQBitName(getQBitMetaData().getName()) : item);
|
||||
}
|
||||
if(!Boolean.FALSE.equals(qBitConfig.getAppEnabled()))
|
||||
{
|
||||
output.add(CrmAppMetaData.build(output, getQBitMetaData().getName()));
|
||||
}
|
||||
output.add(new QAlertTypeMetaData().withName("crm.lead.received").withLabel("New web lead")
|
||||
.withDefaultSeverity(AlertSeverity.INFO).withSourceQBitName(getQBitMetaData().getName()));
|
||||
output.add(new QAlertTypeMetaData().withName("crm.sequence.failing").withLabel("Sequence needs attention")
|
||||
.withSourceQBitName(getQBitMetaData().getName()));
|
||||
output.add(new QAlertTypeMetaData().withName("crm.email.unavailable").withLabel("Email unavailable")
|
||||
.withSourceQBitName(getQBitMetaData().getName()));
|
||||
for(String notification : List.of("crm.lead.assigned", "crm.deal.won", "crm.deal.rotting", "crm.task.due"))
|
||||
{
|
||||
output.add(new QAlertTypeMetaData().withName(notification).withLabel(notification)
|
||||
.withDefaultSeverity(AlertSeverity.INFO).withSourceQBitName(getQBitMetaData().getName()));
|
||||
}
|
||||
qInstance.withProcessLifecycleListener(new QCodeReference(CrmEmailProcessIdentity.class));
|
||||
registerHooks(qInstance);
|
||||
// Registration is metadata-only; core startup sync guards hosts without helpContent.
|
||||
HelpBundles.register(qInstance, "help/qbit-crm.yaml");
|
||||
return (output);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Link the route provider only when both optional middleware classes exist.
|
||||
*******************************************************************************/
|
||||
private static void registerHooks(QInstance instance)
|
||||
{
|
||||
try
|
||||
{
|
||||
Class.forName("io.javalin.Javalin");
|
||||
Class.forName("com.kingsrook.qqq.middleware.javalin.QJavalinMetaData");
|
||||
}
|
||||
catch(ClassNotFoundException | LinkageError e)
|
||||
{
|
||||
return;
|
||||
}
|
||||
com.kingsrook.qbits.crm.webhook.CrmHookRouteProvider.register(instance);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,7 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class for null-safe session user ID resolution. Used by processes
|
||||
** and customizers that need the current user ID for audit logging.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm;
|
||||
|
||||
|
||||
|
||||
@@ -1,656 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package com.kingsrook.qbits.crm.actions;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.crm.activities.processes.LogActivityProcess;
|
||||
import com.kingsrook.qbits.crm.api.CrmApi;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmEnrollmentStatus;
|
||||
import com.kingsrook.qbits.crm.core.processes.ConvertFormToContactProcess;
|
||||
import com.kingsrook.qbits.crm.core.processes.GdprAnonymizeContactProcess;
|
||||
import com.kingsrook.qbits.crm.core.processes.MergeCompaniesProcess;
|
||||
import com.kingsrook.qbits.crm.core.processes.MergeContactsProcess;
|
||||
import com.kingsrook.qbits.crm.core.processes.SubscribeToListProcess;
|
||||
import com.kingsrook.qbits.crm.core.processes.UnsubscribeFromListProcess;
|
||||
import com.kingsrook.qbits.crm.deals.processes.CloneDealProcess;
|
||||
import com.kingsrook.qbits.crm.deals.processes.ConvertLeadProcess;
|
||||
import com.kingsrook.qbits.crm.deals.processes.ManageContactOnDealProcess;
|
||||
import com.kingsrook.qbits.crm.email.CrmEmailTransport;
|
||||
import com.kingsrook.qbits.crm.email.CrmEmails;
|
||||
import com.kingsrook.qbits.crm.email.processes.EnrollInSequenceProcess;
|
||||
import com.kingsrook.qbits.crm.email.processes.UnenrollFromSequenceProcess;
|
||||
import com.kingsrook.qbits.crm.scoring.processes.AutoAssignProcess;
|
||||
import com.kingsrook.qbits.crm.scoring.processes.CalculateLeadScoresProcess;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.Status;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractLoadStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ProcessSummaryProviderInterface;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Shared streamed action lifecycle. Review never invokes a mutating operation.
|
||||
*******************************************************************************/
|
||||
public final class CrmAction
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(CrmAction.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility holder.
|
||||
*******************************************************************************/
|
||||
private CrmAction()
|
||||
{
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Pin extraction to metadata and require an explicit record selection/filter.
|
||||
*******************************************************************************/
|
||||
public static class Extract extends ExtractViaQueryStep
|
||||
{
|
||||
@Override
|
||||
public void preRun(RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
String table = CrmActions.action(input.getProcessName()).table();
|
||||
input.addValue(StreamedETLWithFrontendProcess.FIELD_SOURCE_TABLE, table);
|
||||
input.addValue(StreamedETLWithFrontendProcess.FIELD_DESTINATION_TABLE, table);
|
||||
super.preRun(input, output);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Reuse the core selection parser for preparation and merge validation.
|
||||
*******************************************************************************/
|
||||
QQueryFilter selection(RunBackendStepInput input) throws QException
|
||||
{
|
||||
if(!StringUtils.hasContent(input.getValueString("queryFilterJson"))
|
||||
&& (input.getCallback() == null || input.getCallback().getQueryFilter() == null))
|
||||
{
|
||||
throw (new QUserFacingException("Select records before running this action"));
|
||||
}
|
||||
return (super.getQueryFilter(input).clone());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected QQueryFilter getQueryFilter(RunBackendStepInput input) throws QException
|
||||
{
|
||||
return (selection(input));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Populate read-only pipeline context and the merge survivor dropdown.
|
||||
*******************************************************************************/
|
||||
public static class Prepare implements BackendStep
|
||||
{
|
||||
@Override
|
||||
public void run(RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
CrmActions.Action action = CrmActions.action(input.getProcessName());
|
||||
QQueryFilter filter = new Extract().selection(input);
|
||||
if(action.operation().equals("merge"))
|
||||
{
|
||||
List<QRecord> rows = selectedPair(action, input);
|
||||
output.addValue("mergeIds", new ArrayList<>(rows.stream().map(r -> r.getValue("id")).toList()));
|
||||
output.addValue("primaryId", rows.getFirst().getValue("id"));
|
||||
}
|
||||
else
|
||||
{
|
||||
List<QRecord> rows = new QueryAction().execute(new QueryInput(action.table()).withFilter(filter.withLimit(2))
|
||||
.withFieldNamesToInclude(Set.of("pipelineId")).withSelectDistinct(true)).getRecords();
|
||||
List<Serializable> pipelines = rows.stream().map(r -> r.getValue("pipelineId")).distinct().toList();
|
||||
if(pipelines.size() != 1)
|
||||
{
|
||||
throw (new QUserFacingException("Select deals from one pipeline to choose a target stage"));
|
||||
}
|
||||
output.addValue("pipelineId", pipelines.getFirst());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Validate and describe each selected row, preserving keys for standard links.
|
||||
*******************************************************************************/
|
||||
public static class Transform extends AbstractTransformStep implements ProcessSummaryProviderInterface
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Resolve the host transport; tests may supply a fake.
|
||||
*******************************************************************************/
|
||||
protected CrmEmailTransport emailTransport() throws QException
|
||||
{
|
||||
return (new CrmEmailTransport(CrmApi.config()));
|
||||
}
|
||||
|
||||
private final Map<String, ProcessSummaryLine> lines = new LinkedHashMap<>();
|
||||
private final Map<String, List<Serializable>> duplicateGroups = new LinkedHashMap<>();
|
||||
private List<QRecord> pair;
|
||||
|
||||
@Override
|
||||
public void preRun(RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
CrmActions.Action action = CrmActions.action(input.getProcessName());
|
||||
if(action.operation().equals("merge"))
|
||||
{
|
||||
pair = selectedPair(action, input);
|
||||
if(pair.stream().noneMatch(r -> Objects.equals(r.getValue("id"), input.getValue("primaryId"))))
|
||||
{
|
||||
throw (new QUserFacingException("The record to keep must be one of the two selected records"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void runOnePage(RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
CrmActions.Action action = CrmActions.action(input.getProcessName());
|
||||
for(QRecord extracted : input.getRecords())
|
||||
{
|
||||
Serializable id = extracted.getValue("id");
|
||||
try
|
||||
{
|
||||
QRecord row = requireRow(action, input, id);
|
||||
String reason = check(action, input, row, action.operation().equals("email") ? emailTransport() : null);
|
||||
if(reason != null)
|
||||
{
|
||||
add(lines, Status.WARNING, reason, reason, id);
|
||||
continue;
|
||||
}
|
||||
if(action.operation().equals("merge"))
|
||||
{
|
||||
if(!Objects.equals(id, input.getValue("primaryId")))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Serializable secondary = pair.stream().map(r -> r.getValue("id")).filter(key -> !key.equals(id)).findFirst().orElseThrow();
|
||||
requireRow(action, input, secondary);
|
||||
row.setValue("mergeSecondaryId", secondary);
|
||||
}
|
||||
if(action.operation().equals("duplicates"))
|
||||
{
|
||||
String key = row.getValueString(action.table().equals("crm_contact") ? "email" : "domain");
|
||||
if(StringUtils.hasContent(key))
|
||||
{
|
||||
duplicateGroups.computeIfAbsent(key.toLowerCase(Locale.ROOT), ignored -> new ArrayList<>()).add(id);
|
||||
}
|
||||
}
|
||||
String future = action.operation().equals("auto") ? "will be evaluated for assignment" : "will be " + action.past();
|
||||
add(lines, Status.OK, future, "was " + action.past(), id);
|
||||
output.addRecord(row);
|
||||
}
|
||||
catch(QUserFacingException e)
|
||||
{
|
||||
add(lines, Status.WARNING, e.getMessage(), e.getMessage(), id);
|
||||
}
|
||||
catch(QException | RuntimeException e)
|
||||
{
|
||||
LOG.warn("CRM action validation failed", e);
|
||||
add(lines, Status.ERROR, "cannot be processed; validation failed (see server log)", "was not processed; validation failed (see server log)", id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArrayList<ProcessSummaryLineInterface> getProcessSummary(RunBackendStepOutput output, boolean result)
|
||||
{
|
||||
ArrayList<ProcessSummaryLineInterface> summary = new ArrayList<>(lines.values());
|
||||
int group = 0;
|
||||
for(List<Serializable> ids : duplicateGroups.values())
|
||||
{
|
||||
if(ids.size() > 1)
|
||||
{
|
||||
ProcessSummaryLine line = line(Status.WARNING, "will appear in duplicate group " + (++group), "appeared in duplicate group " + group);
|
||||
ids.forEach(line::incrementCountAndAddPrimaryKey);
|
||||
summary.add(line);
|
||||
}
|
||||
}
|
||||
return (summary);
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Reauthorize and validate at execution time; count only actual outcomes.
|
||||
*******************************************************************************/
|
||||
public static class Load extends AbstractLoadStep implements ProcessSummaryProviderInterface
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Resolve the host transport; tests may supply a fake.
|
||||
*******************************************************************************/
|
||||
protected CrmEmailTransport emailTransport() throws QException
|
||||
{
|
||||
return (new CrmEmailTransport(CrmApi.config()));
|
||||
}
|
||||
|
||||
private final Map<String, ProcessSummaryLine> lines = new LinkedHashMap<>();
|
||||
private boolean duplicates;
|
||||
|
||||
@Override
|
||||
public void runOnePage(RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
CrmActions.Action action = CrmActions.action(input.getProcessName());
|
||||
duplicates = action.operation().equals("duplicates");
|
||||
for(QRecord reviewed : input.getRecords())
|
||||
{
|
||||
Serializable id = reviewed.getValue("id");
|
||||
try
|
||||
{
|
||||
QRecord row = requireRow(action, input, id);
|
||||
String reason = check(action, input, row, action.operation().equals("email") ? emailTransport() : null);
|
||||
if(reason != null)
|
||||
{
|
||||
add(lines, Status.WARNING, reason, reason, id);
|
||||
continue;
|
||||
}
|
||||
row.setValue("mergeSecondaryId", reviewed.getValue("mergeSecondaryId"));
|
||||
String skipped = perform(action, input, row, action.operation().equals("email") ? emailTransport() : null);
|
||||
if(skipped == null)
|
||||
{
|
||||
add(lines, Status.OK, "will be " + action.past(), "was " + action.past(), id);
|
||||
}
|
||||
else
|
||||
{
|
||||
add(lines, Status.WARNING, skipped, skipped, id);
|
||||
}
|
||||
}
|
||||
catch(QException | RuntimeException e)
|
||||
{
|
||||
LOG.warn("CRM action execution failed", e);
|
||||
String reason = e instanceof QUserFacingException ? e.getMessage() : "see server log";
|
||||
add(lines, Status.ERROR, "cannot be processed: " + reason, "failed: " + reason, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArrayList<ProcessSummaryLineInterface> getProcessSummary(RunBackendStepOutput output, boolean result)
|
||||
{
|
||||
ArrayList<ProcessSummaryLineInterface> summary = new ArrayList<>(lines.values());
|
||||
if(getTransformStep() instanceof Transform transform)
|
||||
{
|
||||
if(duplicates)
|
||||
{
|
||||
return (transform.getProcessSummary(output, result));
|
||||
}
|
||||
transform.lines.values().stream().filter(line -> line.getStatus() != Status.OK).forEach(summary::add);
|
||||
}
|
||||
return (summary);
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Read current records under the session; read access alone cannot mutate.
|
||||
*******************************************************************************/
|
||||
private static QRecord requireRow(CrmActions.Action action, RunBackendStepInput input, Serializable id) throws QException
|
||||
{
|
||||
TablePermissionSubType permission = action.operation().equals("duplicates") ? TablePermissionSubType.READ : TablePermissionSubType.EDIT;
|
||||
if(!PermissionsHelper.hasProcessPermission(input, action.name()) || !PermissionsHelper.hasTablePermission(input, action.table(), permission))
|
||||
{
|
||||
throw (new QUserFacingException("Permission denied for this action"));
|
||||
}
|
||||
QRecord row = GetAction.execute(action.table(), id);
|
||||
if(row == null)
|
||||
{
|
||||
throw (new QUserFacingException("Record is unavailable"));
|
||||
}
|
||||
return (row);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Merge only an explicit pair, including when called outside the UI.
|
||||
*******************************************************************************/
|
||||
private static List<QRecord> selectedPair(CrmActions.Action action, RunBackendStepInput input) throws QException
|
||||
{
|
||||
QQueryFilter filter = new Extract().selection(input);
|
||||
filter.setLimit(3);
|
||||
List<QRecord> rows = new QueryAction().execute(new QueryInput(action.table()).withFilter(filter)).getRecords();
|
||||
if(rows.size() != 2)
|
||||
{
|
||||
throw (new QUserFacingException("Select exactly two records to merge"));
|
||||
}
|
||||
return (rows);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Shared read-only checks for Review and Result.
|
||||
*******************************************************************************/
|
||||
private static String check(CrmActions.Action action, RunBackendStepInput input, QRecord row, CrmEmailTransport transport) throws QException
|
||||
{
|
||||
for(QFieldMetaData field : CrmActions.inputFields(action))
|
||||
{
|
||||
Serializable value = input.getValue(field.getName());
|
||||
if(Boolean.TRUE.equals(field.getIsRequired()) && (value == null || value instanceof String text && text.isBlank()))
|
||||
{
|
||||
return ("Supply " + field.getLabel() + " before reviewing");
|
||||
}
|
||||
if(value != null && field.getPossibleValueSourceName() != null && !field.getName().equals("primaryId"))
|
||||
{
|
||||
if(GetAction.execute(field.getPossibleValueSourceName(), value) == null)
|
||||
{
|
||||
return (field.getLabel() + " is unavailable");
|
||||
}
|
||||
}
|
||||
}
|
||||
switch(action.operation())
|
||||
{
|
||||
case "email":
|
||||
return (CrmEmails.skipReason(row, transport));
|
||||
case "subscribe", "unsubscribe":
|
||||
if(!StringUtils.hasContent(row.getValueString("email")))
|
||||
{
|
||||
return ("Contact has no email address");
|
||||
}
|
||||
break;
|
||||
case "enroll":
|
||||
if(Boolean.TRUE.equals(row.getValueBoolean("doNotEmail")))
|
||||
{
|
||||
return ("Contact has opted out of email");
|
||||
}
|
||||
if(!Boolean.TRUE.equals(GetAction.execute("crm_email_sequence", input.getValue("sequenceId")).getValueBoolean("isActive")))
|
||||
{
|
||||
return ("Sequence is not active");
|
||||
}
|
||||
if(!enrollments(input, row).isEmpty())
|
||||
{
|
||||
return ("Contact is already enrolled in this sequence");
|
||||
}
|
||||
break;
|
||||
case "unenroll":
|
||||
if(enrollments(input, row).isEmpty())
|
||||
{
|
||||
return ("Contact has no active enrollment in this sequence");
|
||||
}
|
||||
break;
|
||||
case "stage", "won", "lost", "reopen":
|
||||
QRecord target = targetStage(action, input, row);
|
||||
if(target == null)
|
||||
{
|
||||
return ("No matching target stage exists in the deal's pipeline");
|
||||
}
|
||||
if(!Objects.equals(row.getValue("pipelineId"), target.getValue("pipelineId")))
|
||||
{
|
||||
return ("Target stage does not belong to this deal's pipeline");
|
||||
}
|
||||
if(Objects.equals(row.getValue("pipelineStageId"), target.getValue("id")))
|
||||
{
|
||||
return ("Deal is already in the target stage");
|
||||
}
|
||||
if(action.operation().equals("reopen"))
|
||||
{
|
||||
QRecord current = GetAction.execute("crm_pipeline_stage", row.getValue("pipelineStageId"));
|
||||
if(current == null || !closed(current) || closed(target))
|
||||
{
|
||||
return ("Reopen requires a closed deal and an open target stage");
|
||||
}
|
||||
}
|
||||
if(StringUtils.hasContent(target.getValueString("requiredFieldsJson")))
|
||||
{
|
||||
try
|
||||
{
|
||||
for(Object field : JsonUtils.toObject(target.getValueString("requiredFieldsJson"), List.class))
|
||||
{
|
||||
if(row.getValue(field.toString()) == null)
|
||||
{
|
||||
return ("Required field '" + field + "' must be set before moving to this stage");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(java.io.IOException e)
|
||||
{
|
||||
throw (new QException("Invalid stage required fields", e));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "createDeal":
|
||||
if(query("crm_pipeline_stage", "pipelineId", input.getValue("pipelineId")).isEmpty())
|
||||
{
|
||||
return ("The pipeline has no stages");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return (null);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Terminal stage lookup stays within each selected deal's pipeline.
|
||||
*******************************************************************************/
|
||||
private static QRecord targetStage(CrmActions.Action action, RunBackendStepInput input, QRecord row) throws QException
|
||||
{
|
||||
if(List.of("stage", "reopen").contains(action.operation()))
|
||||
{
|
||||
return (GetAction.execute("crm_pipeline_stage", input.getValue("targetStageId")));
|
||||
}
|
||||
List<QRecord> stages = new QueryAction().execute(new QueryInput("crm_pipeline_stage").withFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("pipelineId", QCriteriaOperator.EQUALS, row.getValue("pipelineId")))
|
||||
.withCriteria(new QFilterCriteria(action.operation().equals("won") ? "isClosedWon" : "isClosedLost", QCriteriaOperator.EQUALS, true))
|
||||
.withOrderBy(new QFilterOrderBy("sortOrder")).withOrderBy(new QFilterOrderBy("id")).withLimit(1))).getRecords();
|
||||
return (stages.isEmpty() ? null : stages.getFirst());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Closed-stage predicate.
|
||||
*******************************************************************************/
|
||||
private static boolean closed(QRecord stage)
|
||||
{
|
||||
return (Boolean.TRUE.equals(stage.getValueBoolean("isClosedWon")) || Boolean.TRUE.equals(stage.getValueBoolean("isClosedLost")));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Invoke the existing business operations only at Load time.
|
||||
*******************************************************************************/
|
||||
private static String perform(CrmActions.Action action, RunBackendStepInput input, QRecord row, CrmEmailTransport transport) throws QException
|
||||
{
|
||||
RunBackendStepInput call = new RunBackendStepInput().withProcessName(input.getProcessName()).withValues(new HashMap<>());
|
||||
for(QFieldMetaData field : CrmActions.inputFields(action))
|
||||
{
|
||||
call.addValue(field.getName(), input.getValue(field.getName()));
|
||||
}
|
||||
Serializable id = row.getValue("id");
|
||||
call.addValue("contactId", action.table().equals("crm_contact") ? id : input.getValue("contactId"));
|
||||
call.addValue("companyId", action.table().equals("crm_company") ? id : null);
|
||||
call.addValue("dealId", action.table().equals("crm_deal") ? id : null);
|
||||
call.addValue("formSubmissionId", id);
|
||||
RunBackendStepOutput result = new RunBackendStepOutput();
|
||||
switch(action.operation())
|
||||
{
|
||||
case "email" ->
|
||||
{
|
||||
String processUUID = input.getValueString("crmEmailProcessUUID");
|
||||
if(processUUID == null)
|
||||
{
|
||||
throw (new QUserFacingException("Email process identity is missing; start the Send Email action again"));
|
||||
}
|
||||
CrmEmails.send(transport, row, input.getValueInteger("emailTemplateId"), input.getValueString("subject"), input.getValueString("body"),
|
||||
"email", id + ":" + processUUID, null);
|
||||
}
|
||||
case "assign" -> update(action.table(), new QRecord().withValue("id", id).withValue("ownerUserId", input.getValue("newOwnerUserId")));
|
||||
case "auto" ->
|
||||
{
|
||||
call.addValue("entityType", "CONTACT");
|
||||
call.addValue("recordId", id);
|
||||
new AutoAssignProcess().run(call, result);
|
||||
if(!"ASSIGNED".equals(result.getValue("action")))
|
||||
{
|
||||
return ("No assignment rule matched; owner unchanged");
|
||||
}
|
||||
}
|
||||
case "subscribe", "unsubscribe" ->
|
||||
{
|
||||
call.addValue("email", row.getValue("email"));
|
||||
call.addValue("firstName", row.getValue("firstName"));
|
||||
call.addValue("subscriptionSource", "CRM record action");
|
||||
if(action.operation().equals("subscribe"))
|
||||
{
|
||||
new SubscribeToListProcess().run(call, result);
|
||||
}
|
||||
else
|
||||
{
|
||||
new UnsubscribeFromListProcess().run(call, result);
|
||||
}
|
||||
if(List.of("NOOP", "NOT_FOUND").contains(result.getValue("action")))
|
||||
{
|
||||
return ("List membership unchanged");
|
||||
}
|
||||
}
|
||||
case "enroll" -> new EnrollInSequenceProcess().run(call, result);
|
||||
case "unenroll" ->
|
||||
{
|
||||
for(QRecord enrollment : enrollments(input, row))
|
||||
{
|
||||
call.addValue("enrollmentId", enrollment.getValue("id"));
|
||||
new UnenrollFromSequenceProcess().run(call, result);
|
||||
}
|
||||
}
|
||||
case "activity" -> new LogActivityProcess().run(call, result);
|
||||
case "score" -> new CalculateLeadScoresProcess().recalculate(row);
|
||||
case "merge" ->
|
||||
{
|
||||
Serializable secondary = row.getValue("mergeSecondaryId");
|
||||
List<QRecord> pair = selectedPair(action, input);
|
||||
if(secondary == null || secondary.equals(id) || pair.stream().noneMatch(r -> Objects.equals(r.getValue("id"), secondary)))
|
||||
{
|
||||
throw (new QUserFacingException("Merge selection changed; review it again"));
|
||||
}
|
||||
requireRow(action, input, secondary);
|
||||
String entity = action.table().equals("crm_contact") ? "Contact" : "Company";
|
||||
call.addValue("primary" + entity + "Id", id);
|
||||
call.addValue("secondary" + entity + "Id", secondary);
|
||||
if(entity.equals("Contact"))
|
||||
{
|
||||
new MergeContactsProcess().run(call, result);
|
||||
}
|
||||
else
|
||||
{
|
||||
new MergeCompaniesProcess().run(call, result);
|
||||
}
|
||||
}
|
||||
case "duplicates" ->
|
||||
{
|
||||
}
|
||||
case "createDeal" -> new ConvertLeadProcess().run(call, result);
|
||||
case "erase" -> new GdprAnonymizeContactProcess().run(call, result);
|
||||
case "stage", "won", "lost", "reopen" ->
|
||||
{
|
||||
QRecord change = new QRecord().withValue("id", id).withValue("pipelineStageId", targetStage(action, input, row).getValue("id"));
|
||||
if(List.of("won", "lost").contains(action.operation()))
|
||||
{
|
||||
change.setValue("winLossReasonId", input.getValue("winLossReasonId"));
|
||||
}
|
||||
update(action.table(), change);
|
||||
}
|
||||
case "clone" -> new CloneDealProcess().run(call, result);
|
||||
case "addContact" ->
|
||||
{
|
||||
call.addValue("action", "ADD");
|
||||
new ManageContactOnDealProcess().run(call, result);
|
||||
}
|
||||
case "convert" -> new ConvertFormToContactProcess().run(call, result);
|
||||
case "activate", "pause" -> update(action.table(), new QRecord().withValue("id", id).withValue("isActive", action.operation().equals("activate")));
|
||||
default -> throw (new QUserFacingException("Action is not available"));
|
||||
}
|
||||
return (null);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Core DML can return record errors; never present those as successful writes.
|
||||
*******************************************************************************/
|
||||
private static void update(String table, QRecord change) throws QException
|
||||
{
|
||||
List<QRecord> records = new UpdateAction().execute(new UpdateInput(table).withRecord(change)).getRecords();
|
||||
for(QRecord record : records)
|
||||
{
|
||||
if(record.getErrors() != null && !record.getErrors().isEmpty())
|
||||
{
|
||||
throw (new QUserFacingException("Update was rejected: " + record.getErrors()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Active enrollments belonging to the selected contact and chosen sequence.
|
||||
*******************************************************************************/
|
||||
private static List<QRecord> enrollments(RunBackendStepInput input, QRecord row) throws QException
|
||||
{
|
||||
return (new QueryAction().execute(new QueryInput("crm_sequence_enrollment").withFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("contactId", QCriteriaOperator.EQUALS, row.getValue("id")))
|
||||
.withCriteria(new QFilterCriteria("sequenceId", QCriteriaOperator.EQUALS, input.getValue("sequenceId")))
|
||||
.withCriteria(new QFilterCriteria("status", QCriteriaOperator.EQUALS, CrmEnrollmentStatus.ACTIVE.getPossibleValueId())))).getRecords());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Session-scoped related rows.
|
||||
*******************************************************************************/
|
||||
private static List<QRecord> query(String table, String field, Serializable value) throws QException
|
||||
{
|
||||
return (new QueryAction().execute(new QueryInput(table).withFilter(new QQueryFilter(new QFilterCriteria(field, QCriteriaOperator.EQUALS, value)))).getRecords());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Standard future and past messages work on both Review and Result.
|
||||
*******************************************************************************/
|
||||
private static ProcessSummaryLine line(Status status, String future, String past)
|
||||
{
|
||||
return (new ProcessSummaryLine(status).withSingularFutureMessage(future).withPluralFutureMessage(future)
|
||||
.withSingularPastMessage(past).withPluralPastMessage(past.startsWith("was ") ? "were " + past.substring(4) : past));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Group equal outcomes and retain primary keys for standard record links.
|
||||
*******************************************************************************/
|
||||
private static void add(Map<String, ProcessSummaryLine> lines, Status status, String future, String past, Serializable id)
|
||||
{
|
||||
lines.computeIfAbsent(status + ":" + future, ignored -> line(status, future, past)).incrementCountAndAddPrimaryKey(id);
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package com.kingsrook.qbits.crm.actions;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.crm.security.CrmPermissions;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** CRM record actions. The installed definition owns the table and operation.
|
||||
*******************************************************************************/
|
||||
public class CrmActions implements MetaDataProducerInterface<MetaDataProducerMultiOutput>
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Immutable action definition; no session state is shared between runs.
|
||||
*******************************************************************************/
|
||||
public record Action(String name, String table, String label, String operation, String past)
|
||||
{
|
||||
}
|
||||
|
||||
public static final List<Action> ACTIONS = List.of(
|
||||
new Action("crm_bulk_assign_owner", "crm_contact", "Assign Owner", "assign", "assigned a new owner"),
|
||||
new Action("crm_auto_assign", "crm_contact", "Auto-Assign", "auto", "auto-assigned"),
|
||||
new Action("crm_subscribe_to_list", "crm_contact", "Add to List", "subscribe", "added to the list"),
|
||||
new Action("crm_unsubscribe_from_list", "crm_contact", "Remove from List", "unsubscribe", "removed from the list"),
|
||||
new Action("crm_enroll_in_sequence", "crm_contact", "Enroll in Sequence", "enroll", "enrolled in the sequence"),
|
||||
new Action("crm_unenroll_from_sequence", "crm_contact", "Unenroll from Sequence", "unenroll", "unenrolled from the sequence"),
|
||||
new Action("crm_log_activity", "crm_contact", "Log Activity", "activity", "given a logged activity"),
|
||||
new Action("crm_recalculate_score", "crm_contact", "Recalculate Score", "score", "rescored"),
|
||||
new Action("crm_merge_contacts", "crm_contact", "Merge Contacts", "merge", "merged"),
|
||||
new Action("crm_detect_duplicates", "crm_contact", "Find Duplicates", "duplicates", "checked for duplicates"),
|
||||
new Action("crm_convert_lead", "crm_contact", "Create Deal", "createDeal", "given a new deal"),
|
||||
new Action("crm_gdpr_anonymize_contact", "crm_contact", "Erase Personal Data", "erase", "anonymized"),
|
||||
new Action("crm_send_email", "crm_contact", "Send Email", "email", "sent email"),
|
||||
new Action("crm_assign_company_owner", "crm_company", "Assign Owner", "assign", "assigned a new owner"),
|
||||
new Action("crm_merge_companies", "crm_company", "Merge Companies", "merge", "merged"),
|
||||
new Action("crm_find_duplicate_companies", "crm_company", "Find Duplicates", "duplicates", "checked for duplicates"),
|
||||
new Action("crm_log_company_activity", "crm_company", "Log Activity", "activity", "given a logged activity"),
|
||||
new Action("crm_move_deal_stage", "crm_deal", "Move Stage", "stage", "moved to the target stage"),
|
||||
new Action("crm_mark_deal_won", "crm_deal", "Mark Won", "won", "marked won"),
|
||||
new Action("crm_mark_deal_lost", "crm_deal", "Mark Lost", "lost", "marked lost"),
|
||||
new Action("crm_reopen_deal", "crm_deal", "Reopen", "reopen", "reopened"),
|
||||
new Action("crm_clone_deal", "crm_deal", "Clone", "clone", "cloned"),
|
||||
new Action("crm_transfer_ownership", "crm_deal", "Assign Owner", "assign", "assigned a new owner"),
|
||||
new Action("crm_manage_contact_on_deal", "crm_deal", "Add Contact", "addContact", "linked to the contact"),
|
||||
new Action("crm_log_deal_activity", "crm_deal", "Log Activity", "activity", "given a logged activity"),
|
||||
new Action("crm_convert_form_to_contact", "crm_form_submission", "Convert to Contact", "convert", "converted to contacts"),
|
||||
new Action("crm_activate_sequence", "crm_email_sequence", "Activate", "activate", "activated"),
|
||||
new Action("crm_pause_sequence", "crm_email_sequence", "Pause", "pause", "paused"));
|
||||
|
||||
/*******************************************************************************
|
||||
** Resolve exclusively by the installed process name, never client values.
|
||||
*******************************************************************************/
|
||||
public static Action action(String name)
|
||||
{
|
||||
return (ACTIONS.stream().filter(a -> a.name().equals(name)).findFirst().orElseThrow());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Package-discovered metadata for every UI action.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public MetaDataProducerMultiOutput produce(QInstance instance)
|
||||
{
|
||||
MetaDataProducerMultiOutput output = new MetaDataProducerMultiOutput();
|
||||
ACTIONS.forEach(action -> output.add(metadata(action)));
|
||||
return (output);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Input details precede the standard ETL Review and Result screens.
|
||||
*******************************************************************************/
|
||||
public static QProcessMetaData metadata(Action action)
|
||||
{
|
||||
Map<String, Serializable> values = Map.of(
|
||||
StreamedETLWithFrontendProcess.FIELD_SOURCE_TABLE, action.table(),
|
||||
StreamedETLWithFrontendProcess.FIELD_DESTINATION_TABLE, action.table(),
|
||||
StreamedETLWithFrontendProcess.FIELD_SUPPORTS_FULL_VALIDATION, true,
|
||||
StreamedETLWithFrontendProcess.FIELD_DO_FULL_VALIDATION, true,
|
||||
StreamedETLWithFrontendProcess.FIELD_TRANSACTION_LEVEL, StreamedETLWithFrontendProcess.TRANSACTION_LEVEL_AUTO_COMMIT,
|
||||
StreamedETLWithFrontendProcess.FIELD_PREVIEW_MESSAGE, action.label() + " for the selected records.");
|
||||
QProcessMetaData process = StreamedETLWithFrontendProcess.defineProcessMetaData(CrmAction.Extract.class, CrmAction.Transform.class, CrmAction.Load.class, values)
|
||||
.withName(action.name()).withLabel(action.label()).withTableName(action.table()).withMinInputRecords(1)
|
||||
.withIcon(new QIcon("play_arrow")).withPermissionRules(CrmPermissions.processRules(action.name()));
|
||||
if(action.operation().equals("merge"))
|
||||
{
|
||||
process.withMinInputRecords(2).withMaxInputRecords(2);
|
||||
}
|
||||
List<QFieldMetaData> fields = inputFields(action);
|
||||
if(!fields.isEmpty())
|
||||
{
|
||||
QFrontendStepMetaData details = new QFrontendStepMetaData().withName("details").withLabel(action.label())
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM));
|
||||
fields.forEach(details::withFormField);
|
||||
process.withStep(0, details);
|
||||
if(List.of("stage", "reopen", "merge").contains(action.operation()))
|
||||
{
|
||||
process.withStep(0, new QBackendStepMetaData().withName("prepare").withCode(new QCodeReference(CrmAction.Prepare.class)));
|
||||
}
|
||||
}
|
||||
return (process);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Required user input, including server-validated contingent stage choices.
|
||||
*******************************************************************************/
|
||||
public static List<QFieldMetaData> inputFields(Action action)
|
||||
{
|
||||
List<QFieldMetaData> fields = new ArrayList<>();
|
||||
switch(action.operation())
|
||||
{
|
||||
case "email" ->
|
||||
{
|
||||
fields.add(field("emailTemplateId", "Template", QFieldType.INTEGER, "crm_email_template").withIsRequired(false));
|
||||
fields.add(field("subject", "Subject", QFieldType.STRING, null).withIsRequired(false));
|
||||
fields.add(field("body", "Body", QFieldType.TEXT, null).withIsRequired(false));
|
||||
}
|
||||
case "assign" -> fields.add(field("newOwnerUserId", "Owner", QFieldType.STRING, null));
|
||||
case "subscribe", "unsubscribe" -> fields.add(field("emailListId", "List", QFieldType.INTEGER, "crm_email_list"));
|
||||
case "enroll", "unenroll" -> fields.add(field("sequenceId", "Sequence", QFieldType.INTEGER, "crm_email_sequence"));
|
||||
case "merge" -> fields.add(field("primaryId", "Record to Keep", QFieldType.INTEGER, action.table())
|
||||
.withPossibleValueSourceFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.IN, "${input.mergeIds}"))));
|
||||
case "stage", "reopen" ->
|
||||
{
|
||||
fields.add(field("pipelineId", "Pipeline", QFieldType.INTEGER, "crm_pipeline").withIsEditable(false));
|
||||
fields.add(field("targetStageId", "Target Stage", QFieldType.INTEGER, "crm_pipeline_stage")
|
||||
.withPossibleValueSourceFilter(new QQueryFilter(new QFilterCriteria("pipelineId", QCriteriaOperator.EQUALS, "${input.pipelineId}"))));
|
||||
}
|
||||
case "won", "lost" -> fields.add(field("winLossReasonId", "Win/Loss Reason", QFieldType.INTEGER, "crm_win_loss_reason"));
|
||||
case "createDeal" ->
|
||||
{
|
||||
fields.add(field("pipelineId", "Pipeline", QFieldType.INTEGER, "crm_pipeline"));
|
||||
fields.add(field("dealName", "Deal Name", QFieldType.STRING, null));
|
||||
fields.add(field("dealAmount", "Amount", QFieldType.DECIMAL, null).withIsRequired(false));
|
||||
}
|
||||
case "addContact" ->
|
||||
{
|
||||
fields.add(field("contactId", "Contact", QFieldType.INTEGER, "crm_contact"));
|
||||
fields.add(field("contactRoleId", "Role", QFieldType.INTEGER, "crm_contact_role").withIsRequired(false));
|
||||
fields.add(field("isPrimary", "Primary Contact", QFieldType.BOOLEAN, null).withIsRequired(false));
|
||||
}
|
||||
case "activity" ->
|
||||
{
|
||||
fields.add(field("activityTypeId", "Activity Type", QFieldType.INTEGER, "crm_activity_type"));
|
||||
fields.add(field("subject", "Subject", QFieldType.STRING, null));
|
||||
fields.add(field("description", "Description", QFieldType.TEXT, null).withIsRequired(false));
|
||||
}
|
||||
default ->
|
||||
{
|
||||
}
|
||||
}
|
||||
return (fields);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Consistent required field metadata.
|
||||
*******************************************************************************/
|
||||
private static QFieldMetaData field(String name, String label, QFieldType type, String pvs)
|
||||
{
|
||||
return (new QFieldMetaData(name, type).withLabel(label).withIsRequired(true).withPossibleValueSourceName(pvs));
|
||||
}
|
||||
}
|
||||
-22
@@ -1,30 +1,8 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** POST_INSERT table customizer that propagates lastActivityDate and
|
||||
** lastContactedDate to linked Contact, Company, and Deal records whenever
|
||||
** a new Activity is created.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.activities.customizers;
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for the crm_activity table.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.activities.model;
|
||||
|
||||
|
||||
@@ -35,9 +13,6 @@ import com.kingsrook.qbits.crm.core.model.enums.CrmDirection;
|
||||
import com.kingsrook.qbits.crm.deals.model.Deal;
|
||||
import com.kingsrook.qbits.crm.email.model.EmailMessage;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
@@ -79,7 +54,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||
)
|
||||
public class Activity extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "crm_activity";
|
||||
public static final String TABLE_NAME = "crmActivity";
|
||||
|
||||
|
||||
|
||||
@@ -112,12 +87,6 @@ public class Activity extends QRecordEntity
|
||||
.withSection(SectionFactory.customT2("participants", new QIcon("group")).withWidgetName(participantChildJoinName))
|
||||
.withSection(SectionFactory.defaultT3("externalCalendarId", "createDate", "modifyDate"));
|
||||
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// Only offer outcomes belonging to the selected activity type. //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
table.getField("outcomeId").withPossibleValueSourceFilter(new QQueryFilter(
|
||||
new QFilterCriteria("activityTypeId", QCriteriaOperator.EQUALS, "${input.activityTypeId}")));
|
||||
|
||||
return (table);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for the crm_activity_outcome table.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.activities.model;
|
||||
|
||||
|
||||
@@ -52,7 +30,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
)
|
||||
public class ActivityOutcome extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "crm_activity_outcome";
|
||||
public static final String TABLE_NAME = "crmActivityOutcome";
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for the crm_activity_participant table.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.activities.model;
|
||||
|
||||
|
||||
@@ -51,7 +29,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory;
|
||||
)
|
||||
public class ActivityParticipant extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "crm_activity_participant";
|
||||
public static final String TABLE_NAME = "crmActivityParticipant";
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for the crm_activity_type table.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.activities.model;
|
||||
|
||||
|
||||
@@ -56,7 +34,7 @@ import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage
|
||||
)
|
||||
public class ActivityType extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "crm_activity_type";
|
||||
public static final String TABLE_NAME = "crmActivityType";
|
||||
|
||||
|
||||
|
||||
@@ -77,8 +55,7 @@ public class ActivityType extends QRecordEntity
|
||||
.withRecordLabelFormat("%s")
|
||||
.withRecordLabelFields("name")
|
||||
.withUniqueKey(new UniqueKey("name"))
|
||||
.withUniqueKey(new UniqueKey("code"))
|
||||
.withSection(SectionFactory.defaultT1("id", "name", "code", "iconName"))
|
||||
.withSection(SectionFactory.defaultT1("id", "name", "iconName"))
|
||||
.withSection(SectionFactory.defaultT2("isSystem", "sortOrder", "isActive"))
|
||||
.withSection(SectionFactory.defaultT3("createDate", "modifyDate"));
|
||||
|
||||
@@ -117,9 +94,6 @@ public class ActivityType extends QRecordEntity
|
||||
|
||||
|
||||
|
||||
@QField(maxLength = 50, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String code;
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Integer id;
|
||||
|
||||
@@ -411,33 +385,4 @@ public class ActivityType extends QRecordEntity
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for code
|
||||
*******************************************************************************/
|
||||
public String getCode()
|
||||
{
|
||||
return (this.code);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for code
|
||||
*******************************************************************************/
|
||||
public void setCode(String code)
|
||||
{
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for code
|
||||
*******************************************************************************/
|
||||
public ActivityType withCode(String code)
|
||||
{
|
||||
this.code = code;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*******************************************************************************
|
||||
** Scheduled process that fires reminders for activities with isReminderSet
|
||||
** = true and reminderDate <= now. Clears the reminder flag after processing.
|
||||
*******************************************************************************/
|
||||
package com.kingsrook.qbits.crm.activities.processes;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.crm.activities.model.Activity;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** MetaDataProducer + BackendStep for firing reminders. Intended to run on a
|
||||
** 5-minute schedule. Finds activities with active reminders whose
|
||||
** reminderDate has passed, clears the flag, and would fire notifications
|
||||
** in a production environment.
|
||||
*******************************************************************************/
|
||||
public class FireRemindersProcess implements BackendStep, MetaDataProducerInterface<QProcessMetaData>
|
||||
{
|
||||
public static final String NAME = "fireReminders";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Produce the process metadata.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QProcessMetaData produce(QInstance qInstance) throws QException
|
||||
{
|
||||
return new QProcessMetaData()
|
||||
.withName(NAME)
|
||||
.withLabel("Fire Reminders")
|
||||
.withIcon(new QIcon().withName("alarm"))
|
||||
.withStepList(List.of(
|
||||
new QBackendStepMetaData()
|
||||
.withName("execute")
|
||||
.withCode(new QCodeReference(FireRemindersProcess.class))
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Execute: find activities with due reminders and clear the reminder flag.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void run(RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
///////////////////////////////////////////////
|
||||
// query activities with pending reminders //
|
||||
///////////////////////////////////////////////
|
||||
QueryOutput queryOutput = new QueryAction().execute(
|
||||
new QueryInput(Activity.TABLE_NAME)
|
||||
.withFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("isReminderSet", QCriteriaOperator.EQUALS, true))
|
||||
.withCriteria(new QFilterCriteria("reminderDate", QCriteriaOperator.LESS_THAN_OR_EQUALS, Instant.now()))
|
||||
.withCriteria(new QFilterCriteria("isCompleted", QCriteriaOperator.EQUALS, false))));
|
||||
|
||||
int firedCount = 0;
|
||||
for(QRecord record : queryOutput.getRecords())
|
||||
{
|
||||
/////////////////////////////////////////////////////////////
|
||||
// In production this would fire a notification. For now, //
|
||||
// just clear the reminder flag. //
|
||||
/////////////////////////////////////////////////////////////
|
||||
QRecord updateRecord = new QRecord()
|
||||
.withValue("id", record.getValueInteger("id"))
|
||||
.withValue("isReminderSet", false);
|
||||
|
||||
new UpdateAction().execute(
|
||||
new UpdateInput(Activity.TABLE_NAME).withRecord(updateRecord));
|
||||
|
||||
firedCount++;
|
||||
}
|
||||
|
||||
output.addValue("firedCount", firedCount);
|
||||
}
|
||||
|
||||
}
|
||||
+53
-25
@@ -1,50 +1,78 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Process to log a CRM activity. Creates a single crm_activity record with
|
||||
** the provided fields and sets ownerUserId from the current session user.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.activities.processes;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.crm.activities.model.Activity;
|
||||
import com.kingsrook.qbits.crm.activities.model.ActivityOutcome;
|
||||
import com.kingsrook.qbits.crm.activities.model.ActivityType;
|
||||
import com.kingsrook.qbits.crm.core.model.Company;
|
||||
import com.kingsrook.qbits.crm.core.model.Contact;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmDirection;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Backend operation used by the streamed CRM actions that creates a crm_activity record from
|
||||
** MetaDataProducer + BackendStep that creates a crm_activity record from
|
||||
** the provided input fields. Sets ownerUserId from the current session.
|
||||
*******************************************************************************/
|
||||
public class LogActivityProcess implements BackendStep
|
||||
public class LogActivityProcess implements BackendStep, MetaDataProducerInterface<QProcessMetaData>
|
||||
{
|
||||
public static final String NAME = "crm_log_activity";
|
||||
public static final String NAME = "logActivity";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Produce the process metadata with input fields for all activity fields.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QProcessMetaData produce(QInstance qInstance) throws QException
|
||||
{
|
||||
return new QProcessMetaData()
|
||||
.withName(NAME)
|
||||
.withLabel("Log Activity")
|
||||
.withTableName(Activity.TABLE_NAME)
|
||||
.withIcon(new QIcon().withName("event_note"))
|
||||
.withStepList(List.of(
|
||||
new QBackendStepMetaData()
|
||||
.withName("execute")
|
||||
.withCode(new QCodeReference(LogActivityProcess.class))
|
||||
.withInputData(new QFunctionInputMetaData().withFieldList(List.of(
|
||||
new QFieldMetaData("activityTypeId", QFieldType.INTEGER).withIsRequired(true).withPossibleValueSourceName(ActivityType.TABLE_NAME),
|
||||
new QFieldMetaData("subject", QFieldType.STRING).withIsRequired(true),
|
||||
new QFieldMetaData("description", QFieldType.STRING),
|
||||
new QFieldMetaData("contactId", QFieldType.INTEGER).withPossibleValueSourceName(Contact.TABLE_NAME),
|
||||
new QFieldMetaData("companyId", QFieldType.INTEGER).withPossibleValueSourceName(Company.TABLE_NAME),
|
||||
new QFieldMetaData("dealId", QFieldType.INTEGER),
|
||||
new QFieldMetaData("direction", QFieldType.INTEGER).withPossibleValueSourceName(CrmDirection.NAME),
|
||||
new QFieldMetaData("durationMinutes", QFieldType.INTEGER),
|
||||
new QFieldMetaData("outcomeId", QFieldType.INTEGER).withPossibleValueSourceName(ActivityOutcome.TABLE_NAME),
|
||||
new QFieldMetaData("location", QFieldType.STRING),
|
||||
new QFieldMetaData("conferenceLink", QFieldType.STRING),
|
||||
new QFieldMetaData("startDate", QFieldType.DATE_TIME),
|
||||
new QFieldMetaData("endDate", QFieldType.DATE_TIME)
|
||||
)))
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
/*******************************************************************************
|
||||
** Scheduled process that finds overdue tasks and marks them with a system
|
||||
** note. In production this would fire notifications; for now it logs a
|
||||
** message on each overdue activity.
|
||||
*******************************************************************************/
|
||||
package com.kingsrook.qbits.crm.activities.processes;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.crm.activities.model.Activity;
|
||||
import com.kingsrook.qbits.crm.activities.model.ActivityType;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** MetaDataProducer + BackendStep for processing overdue tasks. Intended to
|
||||
** run on an hourly schedule. Finds activities of type "Task" with dueDate
|
||||
** in the past and isCompleted=false.
|
||||
*******************************************************************************/
|
||||
public class ProcessOverdueTasksProcess implements BackendStep, MetaDataProducerInterface<QProcessMetaData>
|
||||
{
|
||||
public static final String NAME = "processOverdueTasks";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Produce the process metadata.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QProcessMetaData produce(QInstance qInstance) throws QException
|
||||
{
|
||||
return new QProcessMetaData()
|
||||
.withName(NAME)
|
||||
.withLabel("Process Overdue Tasks")
|
||||
.withIcon(new QIcon().withName("warning"))
|
||||
.withStepList(List.of(
|
||||
new QBackendStepMetaData()
|
||||
.withName("execute")
|
||||
.withCode(new QCodeReference(ProcessOverdueTasksProcess.class))
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Execute: find overdue tasks and mark them with a system note.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void run(RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
///////////////////////////////////////////////
|
||||
// look up the "Task" activity type //
|
||||
///////////////////////////////////////////////
|
||||
Integer taskTypeId = lookupTaskActivityTypeId();
|
||||
if(taskTypeId == null)
|
||||
{
|
||||
output.addValue("processedCount", 0);
|
||||
return;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// query overdue, incomplete tasks //
|
||||
///////////////////////////////////////////////
|
||||
QueryOutput queryOutput = new QueryAction().execute(
|
||||
new QueryInput(Activity.TABLE_NAME)
|
||||
.withFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("activityTypeId", QCriteriaOperator.EQUALS, taskTypeId))
|
||||
.withCriteria(new QFilterCriteria("dueDate", QCriteriaOperator.LESS_THAN, Instant.now()))
|
||||
.withCriteria(new QFilterCriteria("isCompleted", QCriteriaOperator.EQUALS, false))));
|
||||
|
||||
int processedCount = 0;
|
||||
for(QRecord record : queryOutput.getRecords())
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// In production this would create a notification. For now, add a //
|
||||
// system note to the description indicating it is overdue. //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
String currentDescription = record.getValueString("description");
|
||||
String note = "[OVERDUE] Task past due date.";
|
||||
String newDescription = currentDescription != null ? currentDescription + " " + note : note;
|
||||
|
||||
QRecord updateRecord = new QRecord()
|
||||
.withValue("id", record.getValueInteger("id"))
|
||||
.withValue("description", newDescription);
|
||||
|
||||
new UpdateAction().execute(
|
||||
new UpdateInput(Activity.TABLE_NAME).withRecord(updateRecord));
|
||||
|
||||
processedCount++;
|
||||
}
|
||||
|
||||
output.addValue("processedCount", processedCount);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Look up the "Task" activity type id. Returns null if not found.
|
||||
***************************************************************************/
|
||||
private Integer lookupTaskActivityTypeId() throws QException
|
||||
{
|
||||
QueryOutput queryOutput = new QueryAction().execute(
|
||||
new QueryInput(ActivityType.TABLE_NAME)
|
||||
.withFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "Task"))));
|
||||
|
||||
if(!queryOutput.getRecords().isEmpty())
|
||||
{
|
||||
return (queryOutput.getRecords().get(0).getValueInteger("id"));
|
||||
}
|
||||
|
||||
return (null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.crm.api;
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Stable activity codes seeded on first use.
|
||||
*******************************************************************************/
|
||||
public enum ActivityKind
|
||||
{
|
||||
NOTE,
|
||||
EMAIL,
|
||||
CALL,
|
||||
MEETING,
|
||||
FORM_SUBMISSION,
|
||||
SEQUENCE_STEP
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.crm.api;
|
||||
|
||||
|
||||
import com.kingsrook.qbits.crm.CrmQBitConfig;
|
||||
import com.kingsrook.qbits.crm.CrmQBitProducer;
|
||||
import com.kingsrook.qbits.crm.email.CrmEmailTransport;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** CRM installation and email readiness for hosts and integrations.
|
||||
*******************************************************************************/
|
||||
public final class CrmApi
|
||||
{
|
||||
/*******************************************************************************
|
||||
** CRM facade cannot be instantiated.
|
||||
*******************************************************************************/
|
||||
private CrmApi()
|
||||
{
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether the CRM metadata is installed in the supplied instance.
|
||||
*******************************************************************************/
|
||||
public static boolean isInstalled(QInstance instance)
|
||||
{
|
||||
return (instance != null && instance.getTable("crm_contact") != null && instance.getTable("crm_consent_record") != null
|
||||
&& instance.getTable("crm_email_list_member") != null && instance.getTable("crm_form_submission") != null);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Report the currently configured email transport.
|
||||
*******************************************************************************/
|
||||
public static CrmReadiness readiness() throws QException
|
||||
{
|
||||
return (isInstalled(QContext.getQInstance()) ? new CrmEmailTransport(config()).readiness() : CrmReadiness.NOT_CONFIGURED);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Resolve configuration from the current instance, never global mutable state.
|
||||
*******************************************************************************/
|
||||
public static CrmQBitConfig config() throws QUserFacingException
|
||||
{
|
||||
var bit = QContext.getQInstance().getQBits().get(CrmQBitProducer.GROUP_ID + ":" + CrmQBitProducer.ARTIFACT_ID);
|
||||
if(bit == null || !(bit.getConfig() instanceof CrmQBitConfig config))
|
||||
{
|
||||
throw (new QUserFacingException("CRM is not installed in this application."));
|
||||
}
|
||||
return (config);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.crm.api;
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Website submission. visitorKey and clientHash must already be SHA-256 hashes; never pass raw identifiers.
|
||||
*******************************************************************************/
|
||||
public record CrmFormRequest(String formKey, FormType formType, String firstName, String lastName, String email, String phone, String company,
|
||||
String message, boolean marketingConsent, String consentText, String utmSource, String utmMedium, String utmCampaign,
|
||||
String utmContent, String utmTerm, String landingPage, String referrer, String visitorKey, String clientHash)
|
||||
{
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.crm.api;
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Persisted submission and linked contact identifiers.
|
||||
*******************************************************************************/
|
||||
public record CrmFormResult(Integer submissionId, Integer contactId, boolean contactCreated)
|
||||
{
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.crm.api;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Campaign attribution reference containing identifiers and time only.
|
||||
*******************************************************************************/
|
||||
public record CrmFormSubmissionRef(Integer submissionId, Integer contactId, Instant submittedAt)
|
||||
{
|
||||
}
|
||||
@@ -1,400 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.crm.api;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qbits.crm.CrmQBitConfig;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmAssignableEntity;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmConsentStatus;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmConsentType;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmFormConversionStatus;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmFormType;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmLegalBasis;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmListMemberStatus;
|
||||
import com.kingsrook.qbits.crm.email.CrmEmailTransport;
|
||||
import com.kingsrook.qbits.crm.scoring.CrmAutoAssign;
|
||||
import com.kingsrook.qbits.crm.security.CrmTokens;
|
||||
import com.kingsrook.qqq.backend.core.alerts.NotificationAudience;
|
||||
import com.kingsrook.qqq.backend.core.alerts.NotificationSignal;
|
||||
import com.kingsrook.qqq.backend.core.alerts.QAlerts;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.CriteriaOption;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Trusted host work: the host or signed public hook validates requests before entry, so caller CRM permissions are not required.
|
||||
** Website intake, evidence-backed consent, double opt-in and signed unsubscribe operations under the current QContext.
|
||||
*******************************************************************************/
|
||||
public class CrmIntakeApi
|
||||
{
|
||||
private final CrmStore store;
|
||||
private final CrmQBitConfig config;
|
||||
private final CrmEmailTransport transport;
|
||||
|
||||
/*******************************************************************************
|
||||
** Use the current instance configuration and standard transport.
|
||||
*******************************************************************************/
|
||||
public CrmIntakeApi() throws QException
|
||||
{
|
||||
this(CrmApi.config(), null);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Supply a transport for host integration or deterministic delivery tests.
|
||||
*******************************************************************************/
|
||||
public CrmIntakeApi(CrmQBitConfig config, CrmEmailTransport transport)
|
||||
{
|
||||
this(config, transport, CrmStore.trusted());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Preserve enforcement when the signed-in marketing API delegates a withdrawal.
|
||||
*******************************************************************************/
|
||||
CrmIntakeApi(CrmQBitConfig config, CrmEmailTransport transport, CrmStore store)
|
||||
{
|
||||
this.store = store;
|
||||
this.config = config;
|
||||
this.transport = transport == null ? new CrmEmailTransport(config) : transport;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Store a form and attribution, recording marketing consent only for explicit opt-in.
|
||||
*******************************************************************************/
|
||||
public CrmFormResult submitForm(CrmFormRequest request) throws QException
|
||||
{
|
||||
validate(request);
|
||||
CrmStarterData.ensure(store);
|
||||
List<QRecord> contacts = contacts(request.email());
|
||||
boolean created = contacts.isEmpty();
|
||||
QRecord contact = contact(request, contacts);
|
||||
Integer id = contact.getValueInteger("id");
|
||||
QRecord submission = submission(request, id);
|
||||
consent(id, request.marketingConsent() ? CrmConsentType.EMAIL : CrmConsentType.REPLY_TO_INQUIRY, CrmConsentStatus.GRANTED,
|
||||
request.marketingConsent() ? CrmLegalBasis.CONSENT : CrmLegalBasis.LEGITIMATE_INTEREST,
|
||||
request.formKey(), request.consentText(), submission.getValueInteger("id"));
|
||||
store.recordActivity(id, ActivityKind.FORM_SUBMISSION, "Form submission", request.message());
|
||||
notifyLead(submission, NotificationAudience.permission("crm.read"));
|
||||
if(!isUnassigned(contact.getValueString("ownerUserId")))
|
||||
{
|
||||
notifyLead(submission, NotificationAudience.user(contact.getValueString("ownerUserId")));
|
||||
}
|
||||
return (new CrmFormResult(submission.getValueInteger("id"), id, created));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Subscribe to an active list. Pending confirmation never grants Email consent.
|
||||
*******************************************************************************/
|
||||
public CrmSubscribeResult subscribe(CrmSubscribeRequest request) throws QException
|
||||
{
|
||||
if(request == null || request.listKey() == null || request.listKey().isBlank())
|
||||
{
|
||||
throw (new QUserFacingException("A mailing list key is required."));
|
||||
}
|
||||
List<QRecord> lists = store.by("crm_email_list", "listKey", request.listKey());
|
||||
if(lists.isEmpty() || !Boolean.TRUE.equals(lists.get(0).getValueBoolean("isActive")))
|
||||
{
|
||||
throw (new QUserFacingException("This mailing list is unavailable."));
|
||||
}
|
||||
QRecord list = lists.get(0);
|
||||
Integer listId = list.getValueInteger("id");
|
||||
String wording = request.consentText() == null || request.consentText().isBlank() ? list.getValueString("consentText") : request.consentText();
|
||||
CrmFormRequest form = new CrmFormRequest(request.listKey(), FormType.NEWSLETTER, request.firstName(), request.lastName(),
|
||||
request.email(), null, null, null, true, wording, request.utmSource(), request.utmMedium(), request.utmCampaign(),
|
||||
request.utmContent(), request.utmTerm(), null, null, request.visitorKey(), request.clientHash());
|
||||
validate(form);
|
||||
List<QRecord> contacts = contacts(request.email());
|
||||
QRecord member = null;
|
||||
if(!contacts.isEmpty())
|
||||
{
|
||||
List<QRecord> members = store.query("crm_email_list_member", new QQueryFilter(
|
||||
new QFilterCriteria("emailListId", QCriteriaOperator.EQUALS, listId),
|
||||
new QFilterCriteria("contactId", QCriteriaOperator.EQUALS, contacts.get(0).getValueInteger("id"))));
|
||||
member = members.isEmpty() ? null : members.get(0);
|
||||
if(member != null && CrmListMemberStatus.SUBSCRIBED.getId().equals(member.getValueInteger("status")))
|
||||
{
|
||||
return (new CrmSubscribeResult(SubscriptionStatus.ALREADY_SUBSCRIBED));
|
||||
}
|
||||
}
|
||||
boolean doubleOptIn = !Boolean.FALSE.equals(list.getValueBoolean("requireDoubleOptIn"));
|
||||
CrmTokens tokens = doubleOptIn ? new CrmTokens(config) : null;
|
||||
if(doubleOptIn)
|
||||
{
|
||||
tokens.url("confirm", "configuration-check");
|
||||
if(transport.readiness() == CrmReadiness.NOT_CONFIGURED)
|
||||
{
|
||||
throw (new QUserFacingException("CRM email is not configured for list confirmation."));
|
||||
}
|
||||
}
|
||||
CrmStarterData.ensure(store);
|
||||
QRecord contact = contact(form, contacts);
|
||||
Integer contactId = contact.getValueInteger("id");
|
||||
QRecord submission = submission(form, contactId);
|
||||
QRecord values = new QRecord().withValue("emailListId", listId).withValue("contactId", contactId)
|
||||
.withValue("email", normalize(request.email())).withValue("firstName", request.firstName())
|
||||
.withValue("status", (doubleOptIn ? CrmListMemberStatus.PENDING_CONFIRMATION : CrmListMemberStatus.SUBSCRIBED).getId()).withValue("source", request.source()).withValue("consentText", wording)
|
||||
.withValue("formSubmissionId", submission.getValueInteger("id")).withValue("unsubscribedDate", null)
|
||||
.withValue("subscribedDate", doubleOptIn ? null : Instant.now()).withValue("confirmedDate", null);
|
||||
if(member == null)
|
||||
{
|
||||
member = store.insert("crm_email_list_member", values);
|
||||
}
|
||||
else
|
||||
{
|
||||
member = store.update("crm_email_list_member", values.withValue("id", member.getValueInteger("id")));
|
||||
}
|
||||
Integer memberId = member.getValueInteger("id");
|
||||
if(doubleOptIn)
|
||||
{
|
||||
String token = tokens.issue("confirm", memberId, listId);
|
||||
String hash = CrmTokens.hash(token);
|
||||
store.update("crm_email_list_member", new QRecord().withValue("id", memberId)
|
||||
.withValue("confirmationTokenHash", hash).withValue("confirmationSentDate", Instant.now()));
|
||||
transport.send("confirm", memberId + ":" + hash, contactId, normalize(request.email()), "Confirm your subscription",
|
||||
"Please confirm your subscription: " + tokens.url("confirm", token), true);
|
||||
return (new CrmSubscribeResult(SubscriptionStatus.PENDING_CONFIRMATION));
|
||||
}
|
||||
consent(contactId, CrmConsentType.EMAIL, CrmConsentStatus.GRANTED, CrmLegalBasis.CONSENT, request.source(), wording, submission.getValueInteger("id"));
|
||||
store.update("crm_contact", new QRecord().withValue("id", contactId).withValue("unsubscribeDate", null));
|
||||
return (new CrmSubscribeResult(SubscriptionStatus.SUBSCRIBED));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Confirm the current token only. Repeated confirmation is harmless; refreshed and withdrawn tokens fail.
|
||||
*******************************************************************************/
|
||||
public void confirm(String token) throws QException
|
||||
{
|
||||
CrmTokens.Claims claims = new CrmTokens(config).verify(token, "confirm");
|
||||
QRecord member = store.get("crm_email_list_member", claims.id());
|
||||
if(!Objects.equals(claims.listId(), member.getValueInteger("emailListId"))
|
||||
|| !Objects.equals(CrmTokens.hash(token), member.getValueString("confirmationTokenHash")))
|
||||
{
|
||||
throw (new QUserFacingException("This confirmation link is no longer available. Please subscribe again."));
|
||||
}
|
||||
if(CrmListMemberStatus.SUBSCRIBED.getId().equals(member.getValueInteger("status")))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if(!CrmListMemberStatus.PENDING_CONFIRMATION.getId().equals(member.getValueInteger("status"))
|
||||
|| !Boolean.TRUE.equals(store.get("crm_email_list", claims.listId()).getValueBoolean("isActive")))
|
||||
{
|
||||
throw (new QUserFacingException("This subscription cannot be confirmed."));
|
||||
}
|
||||
store.get("crm_contact", member.getValueInteger("contactId"));
|
||||
consent(member.getValueInteger("contactId"), CrmConsentType.EMAIL, CrmConsentStatus.GRANTED, CrmLegalBasis.CONSENT, member.getValueString("source"), member.getValueString("consentText"), member.getValueInteger("formSubmissionId"));
|
||||
store.update("crm_contact", new QRecord().withValue("id", member.getValueInteger("contactId")).withValue("unsubscribeDate", null));
|
||||
store.update("crm_email_list_member", new QRecord().withValue("id", claims.id()).withValue("status", CrmListMemberStatus.SUBSCRIBED.getId())
|
||||
.withValue("confirmedDate", Instant.now()).withValue("subscribedDate", Instant.now()));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Apply a signed list-level or contact-level unsubscribe.
|
||||
*******************************************************************************/
|
||||
public void unsubscribe(String token) throws QException
|
||||
{
|
||||
CrmTokens.Claims claims = new CrmTokens(config).verify(token, "unsubscribe");
|
||||
unsubscribeByContact(claims.id(), claims.listId(), "Signed unsubscribe link");
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Withdraw consent and mark the selected memberships. Null listId also suppresses the contact globally.
|
||||
*******************************************************************************/
|
||||
public void unsubscribeByContact(Integer contactId, Integer listId, String source) throws QException
|
||||
{
|
||||
store.get("crm_contact", contactId);
|
||||
QQueryFilter filter = new QQueryFilter(new QFilterCriteria("contactId", QCriteriaOperator.EQUALS, contactId));
|
||||
if(listId != null)
|
||||
{
|
||||
store.get("crm_email_list", listId);
|
||||
filter.addCriteria(new QFilterCriteria("emailListId", QCriteriaOperator.EQUALS, listId));
|
||||
}
|
||||
for(QRecord member : store.query("crm_email_list_member", filter))
|
||||
{
|
||||
store.update("crm_email_list_member", new QRecord().withValue("id", member.getValueInteger("id"))
|
||||
.withValue("status", CrmListMemberStatus.UNSUBSCRIBED.getId()).withValue("unsubscribedDate", Instant.now()).withValue("confirmationTokenHash", null));
|
||||
}
|
||||
if(listId == null)
|
||||
{
|
||||
store.update("crm_contact", new QRecord().withValue("id", contactId).withValue("unsubscribeDate", Instant.now()));
|
||||
}
|
||||
consent(contactId, CrmConsentType.EMAIL, CrmConsentStatus.WITHDRAWN, CrmLegalBasis.CONSENT, source, "Email consent withdrawn", null);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Validate before any writes, including the caller's pre-hashed pseudonymous identifiers.
|
||||
*******************************************************************************/
|
||||
private void validate(CrmFormRequest request) throws QUserFacingException
|
||||
{
|
||||
if(request == null || request.formKey() == null || request.formKey().isBlank() || request.formType() == null
|
||||
|| request.email() == null || !normalize(request.email()).matches("[^@\\s]+@[^@\\s]+"))
|
||||
{
|
||||
throw (new QUserFacingException("A form key, form type and valid email address are required."));
|
||||
}
|
||||
if(request.marketingConsent() && (request.consentText() == null || request.consentText().isBlank()))
|
||||
{
|
||||
throw (new QUserFacingException("Marketing consent requires the exact wording shown to the visitor."));
|
||||
}
|
||||
for(String hash : new String[] {request.visitorKey(), request.clientHash()})
|
||||
{
|
||||
if(hash != null && !hash.matches("[a-fA-F0-9]{64}"))
|
||||
{
|
||||
throw (new QUserFacingException("Visitor and client identifiers must be SHA-256 hashes, never raw identifiers."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Resolve email without case sensitivity using the selected storage mode.
|
||||
*******************************************************************************/
|
||||
private List<QRecord> contacts(String email) throws QException
|
||||
{
|
||||
return (store.query("crm_contact", new QQueryFilter(new QFilterCriteria("email", QCriteriaOperator.EQUALS, normalize(email))
|
||||
.withOption(CriteriaOption.CASE_INSENSITIVE))));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Create a lead if needed, then preserve the first touch and refresh last-touch attribution only when supplied.
|
||||
*******************************************************************************/
|
||||
private QRecord contact(CrmFormRequest request, List<QRecord> contacts) throws QException
|
||||
{
|
||||
QRecord contact;
|
||||
if(contacts.isEmpty())
|
||||
{
|
||||
String source = request.utmSource() == null || request.utmSource().isBlank() ? "Website" : request.utmSource().strip();
|
||||
source = source.length() > 100 ? source.substring(0, 100) : source;
|
||||
List<QRecord> sources = store.by("crm_lead_source", "name", source);
|
||||
QRecord leadSource = sources.isEmpty() ? store.insert("crm_lead_source", new QRecord().withValue("name", source).withValue("sortOrder", 0)) : sources.get(0);
|
||||
List<QRecord> stages = store.by("crm_lifecycle_stage", "name", "Lead");
|
||||
contact = store.insert("crm_contact", new QRecord().withValue("email", normalize(request.email()))
|
||||
.withValue("firstName", name(request.firstName())).withValue("lastName", name(request.lastName()))
|
||||
.withValue("phone", request.phone()).withValue("ownerUserId", null)
|
||||
.withValue("leadSourceId", leadSource.getValueInteger("id")).withValue("lifecycleStageId", stages.isEmpty() ? null : stages.get(0).getValueInteger("id")));
|
||||
}
|
||||
else
|
||||
{
|
||||
contact = contacts.get(0);
|
||||
}
|
||||
QRecord update = new QRecord().withValue("id", contact.getValueInteger("id"));
|
||||
String[] suffixes = {"UtmSource", "UtmMedium", "UtmCampaign", "UtmContent", "UtmTerm", "LandingPage", "Referrer"};
|
||||
String[] values = {request.utmSource(), request.utmMedium(), request.utmCampaign(), request.utmContent(), request.utmTerm(), request.landingPage(), request.referrer()};
|
||||
boolean attributed = Arrays.stream(values, 0, 6).anyMatch(value -> value != null && !value.isBlank());
|
||||
boolean first = contact.getValue("firstTouchDate") == null;
|
||||
for(int i = 0; i < suffixes.length; i++)
|
||||
{
|
||||
if(first && contact.getValue("firstTouch" + suffixes[i]) == null)
|
||||
{
|
||||
update.withValue("firstTouch" + suffixes[i], values[i]);
|
||||
}
|
||||
if(attributed)
|
||||
{
|
||||
update.withValue("lastTouch" + suffixes[i], values[i]);
|
||||
}
|
||||
}
|
||||
if(first)
|
||||
{
|
||||
update.withValue("firstTouchDate", Instant.now());
|
||||
}
|
||||
update.withValue("lastTouchDate", Instant.now());
|
||||
store.update("crm_contact", update);
|
||||
if(contacts.isEmpty() && isUnassigned(store.get("crm_contact", contact.getValueInteger("id")).getValueString("ownerUserId")))
|
||||
{
|
||||
CrmAutoAssign.assign(CrmAssignableEntity.CONTACT, contact.getValueInteger("id"));
|
||||
}
|
||||
return (store.get("crm_contact", contact.getValueInteger("id")));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Persist exact submission evidence without raw network identifiers.
|
||||
*******************************************************************************/
|
||||
private QRecord submission(CrmFormRequest request, Integer contactId) throws QException
|
||||
{
|
||||
CrmFormType formType;
|
||||
switch(request.formType())
|
||||
{
|
||||
case CONTACT -> formType = CrmFormType.CONTACT;
|
||||
case DEMO -> formType = CrmFormType.DEMO_REQUEST;
|
||||
case NEWSLETTER -> formType = CrmFormType.NEWSLETTER;
|
||||
default -> formType = CrmFormType.GENERAL;
|
||||
}
|
||||
return (store.insert("crm_form_submission", new QRecord().withValue("formKey", request.formKey()).withValue("formType", formType.getId())
|
||||
.withValue("email", normalize(request.email())).withValue("firstName", request.firstName()).withValue("lastName", request.lastName())
|
||||
.withValue("phone", request.phone()).withValue("company", request.company()).withValue("message", request.message())
|
||||
.withValue("marketingConsent", request.marketingConsent()).withValue("consentText", request.consentText())
|
||||
.withValue("utmSource", request.utmSource()).withValue("utmMedium", request.utmMedium()).withValue("utmCampaign", request.utmCampaign())
|
||||
.withValue("utmContent", request.utmContent()).withValue("utmTerm", request.utmTerm()).withValue("landingPage", request.landingPage())
|
||||
.withValue("referrer", request.referrer()).withValue("visitorKey", request.visitorKey()).withValue("clientHash", request.clientHash())
|
||||
.withValue("contactId", contactId).withValue("conversionStatus", CrmFormConversionStatus.CONVERTED_TO_CONTACT.getId()).withValue("convertedDate", Instant.now())));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Append consent evidence. Existing records remain immutable.
|
||||
*******************************************************************************/
|
||||
private void consent(Integer contactId, CrmConsentType type, CrmConsentStatus status, CrmLegalBasis basis, String source, String wording, Integer submissionId) throws QException
|
||||
{
|
||||
store.insert("crm_consent_record", new QRecord().withValue("contactId", contactId).withValue("consentType", type.getId())
|
||||
.withValue("status", status.getId()).withValue("legalBasis", basis.getId()).withValue("source", source).withValue("consentText", wording)
|
||||
.withValue("evidenceFormSubmissionId", submissionId).withValue("consentDate", Instant.now()));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Core signals accept one audience; publish the same lead to each intended audience.
|
||||
*******************************************************************************/
|
||||
private void notifyLead(QRecord submission, NotificationAudience audience)
|
||||
{
|
||||
QAlerts.notify(new NotificationSignal("New web lead received").withSourceName("crm.lead.received")
|
||||
.withSourceKey("crm.lead.received:" + submission.getValueInteger("id"))
|
||||
.withSourceTableName("crm_form_submission").withSourceRecordId(submission.getValueString("id"))
|
||||
.withAudience(audience));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** System and anonymous placeholders do not represent a sales owner.
|
||||
*******************************************************************************/
|
||||
private boolean isUnassigned(String owner)
|
||||
{
|
||||
return (owner == null || owner.isBlank() || "system".equalsIgnoreCase(owner) || "anonymous".equalsIgnoreCase(owner));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Normalize email for new writes.
|
||||
*******************************************************************************/
|
||||
private String normalize(String email)
|
||||
{
|
||||
return (email.trim().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Existing CRM metadata requires both name fields; retain an honest missing-name label.
|
||||
*******************************************************************************/
|
||||
private String name(String value)
|
||||
{
|
||||
return (value == null || value.isBlank() ? null : value.strip());
|
||||
}
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.crm.api;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmConsentStatus;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmConsentType;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmListMemberStatus;
|
||||
import com.kingsrook.qbits.crm.security.CrmTokens;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Enforces caller permissions because marketing runs for a signed-in user or worker.
|
||||
** Supported marketing contract. Only eligible recipients expose addresses; all other responses contain identifiers or reason codes.
|
||||
*******************************************************************************/
|
||||
public class CrmMarketingApi
|
||||
{
|
||||
private final CrmStore store = CrmStore.asUser();
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether the current application contains the CRM contract tables.
|
||||
*******************************************************************************/
|
||||
public boolean isAvailable()
|
||||
{
|
||||
return (CrmApi.isInstalled(QContext.getQInstance()));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Resolve crm-list:id or crm-contact:id with fresh suppression checks.
|
||||
*******************************************************************************/
|
||||
public List<CrmRecipient> resolveRecipients(String scope) throws QException
|
||||
{
|
||||
List<CrmRecipient> result = new ArrayList<>();
|
||||
if(scope == null)
|
||||
{
|
||||
return (result);
|
||||
}
|
||||
String[] parts = scope.split(":", -1);
|
||||
if(parts.length != 2 || !("crm-list".equals(parts[0]) || "crm-contact".equals(parts[0])))
|
||||
{
|
||||
throw (new QUserFacingException("Use a CRM recipient scope: crm-list:<id> or crm-contact:<id>."));
|
||||
}
|
||||
Integer id;
|
||||
try
|
||||
{
|
||||
id = Integer.valueOf(parts[1]);
|
||||
if(id <= 0)
|
||||
{
|
||||
throw (new NumberFormatException());
|
||||
}
|
||||
}
|
||||
catch(NumberFormatException e)
|
||||
{
|
||||
throw (new QUserFacingException("A CRM recipient scope must end in a positive numeric identifier."));
|
||||
}
|
||||
if("crm-contact".equals(parts[0]))
|
||||
{
|
||||
result.add(recipient(id, null));
|
||||
}
|
||||
else
|
||||
{
|
||||
QRecord list = store.get("crm_email_list", id);
|
||||
for(QRecord member : store.by("crm_email_list_member", "emailListId", id))
|
||||
{
|
||||
Integer contactId = member.getValueInteger("contactId");
|
||||
if(contactId != null)
|
||||
{
|
||||
String reason = !Boolean.TRUE.equals(list.getValueBoolean("isActive")) ? "list_inactive"
|
||||
: CrmListMemberStatus.SUBSCRIBED.getId().equals(member.getValueInteger("status")) ? null : "list_member_not_subscribed";
|
||||
result.add(recipient(contactId, reason));
|
||||
}
|
||||
}
|
||||
}
|
||||
return (result);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Latest Email consent decides; suppression always wins. No consent means no marketing email.
|
||||
*******************************************************************************/
|
||||
public Eligibility checkEmailEligibility(Integer contactId) throws QException
|
||||
{
|
||||
List<QRecord> contacts = contactId == null ? List.of() : store.by("crm_contact", "id", contactId);
|
||||
if(contacts.isEmpty())
|
||||
{
|
||||
return (no("contact_not_found"));
|
||||
}
|
||||
QRecord contact = contacts.get(0);
|
||||
if(Boolean.TRUE.equals(contact.getValueBoolean("doNotEmail")))
|
||||
{
|
||||
return (no("do_not_email"));
|
||||
}
|
||||
if(contact.getValue("unsubscribeDate") != null)
|
||||
{
|
||||
return (no("unsubscribed"));
|
||||
}
|
||||
if(contact.getValueString("email") == null || contact.getValueString("email").isBlank())
|
||||
{
|
||||
return (no("no_email_address"));
|
||||
}
|
||||
List<QRecord> consents = store.query("crm_consent_record", new QQueryFilter(
|
||||
new QFilterCriteria("contactId", QCriteriaOperator.EQUALS, contactId), new QFilterCriteria("consentType", QCriteriaOperator.EQUALS, CrmConsentType.EMAIL.getId())));
|
||||
if(consents.isEmpty())
|
||||
{
|
||||
return (no("no_email_consent"));
|
||||
}
|
||||
QRecord latest = consents.stream().max(Comparator.comparing((QRecord row) -> row.getValueInstant("consentDate") == null
|
||||
? Instant.EPOCH : row.getValueInstant("consentDate")).thenComparing(row -> row.getValueInteger("id"))).orElseThrow();
|
||||
if(!CrmConsentStatus.GRANTED.getId().equals(latest.getValueInteger("status")))
|
||||
{
|
||||
return (no("email_consent_withdrawn"));
|
||||
}
|
||||
Instant expiry = latest.getValueInstant("expiryDate");
|
||||
if(expiry != null && !expiry.isAfter(Instant.now()))
|
||||
{
|
||||
return (no("email_consent_expired"));
|
||||
}
|
||||
return (new Eligibility(true, "eligible"));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Campaign references contain no form contents or addresses.
|
||||
*******************************************************************************/
|
||||
public List<CrmFormSubmissionRef> listFormSubmissions(String utmCampaign, Instant since) throws QException
|
||||
{
|
||||
if(utmCampaign == null || utmCampaign.isBlank())
|
||||
{
|
||||
return (List.of());
|
||||
}
|
||||
QQueryFilter filter = new QQueryFilter(new QFilterCriteria("utmCampaign", QCriteriaOperator.EQUALS, utmCampaign));
|
||||
if(since != null)
|
||||
{
|
||||
filter.addCriteria(new QFilterCriteria("createDate", QCriteriaOperator.GREATER_THAN_OR_EQUALS, since));
|
||||
}
|
||||
return (store.query("crm_form_submission", filter).stream().map(row -> new CrmFormSubmissionRef(row.getValueInteger("id"),
|
||||
row.getValueInteger("contactId"), row.getValueInstant("createDate"))).toList());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Log a typed interaction through the normal activity table actions.
|
||||
*******************************************************************************/
|
||||
public void recordActivity(Integer contactId, ActivityKind kind, String subject, String description) throws QException
|
||||
{
|
||||
store.recordActivity(contactId, kind, subject, description);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Convenience overload for the existing marketing gateway's Note activity contract.
|
||||
*******************************************************************************/
|
||||
public void recordActivity(Integer contactId, String subject, String description) throws QException
|
||||
{
|
||||
recordActivity(contactId, ActivityKind.NOTE, subject, description);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Record an integration-originated withdrawal of email consent.
|
||||
*******************************************************************************/
|
||||
public void recordUnsubscribe(Integer contactId, Integer listId, String source) throws QException
|
||||
{
|
||||
new CrmIntakeApi(CrmApi.config(), null, store).unsubscribeByContact(contactId, listId, source);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Build a ninety-day signed link without returning any personal data.
|
||||
*******************************************************************************/
|
||||
public String buildUnsubscribeUrl(Integer contactId, Integer listId) throws QException
|
||||
{
|
||||
store.get("crm_contact", contactId);
|
||||
if(listId != null)
|
||||
{
|
||||
store.get("crm_email_list", listId);
|
||||
}
|
||||
CrmTokens tokens = new CrmTokens(CrmApi.config());
|
||||
return (tokens.url("unsubscribe", tokens.issue("unsubscribe", contactId, listId)));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Offer only active lists visible to this caller.
|
||||
*******************************************************************************/
|
||||
public List<RecipientScopeOption> recipientScopeOptions() throws QException
|
||||
{
|
||||
return (store.by("crm_email_list", "isActive", true).stream()
|
||||
.map(row -> new RecipientScopeOption("crm-list:" + row.getValueInteger("id"), row.getValueString("name"))).toList());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Fetch an address only after eligibility is established.
|
||||
*******************************************************************************/
|
||||
private CrmRecipient recipient(Integer contactId, String reason) throws QException
|
||||
{
|
||||
Eligibility eligibility = reason == null ? checkEmailEligibility(contactId) : no(reason);
|
||||
return (new CrmRecipient(contactId, eligibility.eligible() ? store.get("crm_contact", contactId).getValueString("email") : null, eligibility));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Construct a suppression result without personal data.
|
||||
*******************************************************************************/
|
||||
private Eligibility no(String reason)
|
||||
{
|
||||
return (new Eligibility(false, reason));
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.crm.api;
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Available email delivery path; configuration is not a delivery guarantee.
|
||||
*******************************************************************************/
|
||||
public enum CrmReadiness
|
||||
{
|
||||
NOT_CONFIGURED,
|
||||
QBIT_EMAIL,
|
||||
CORE_MESSAGING
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.crm.api;
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Recipient address is present only when marketing email is eligible.
|
||||
*******************************************************************************/
|
||||
public record CrmRecipient(Integer contactId, String address, Eligibility eligibility)
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Stable recipient key for marketing executions; never use an address as an id.
|
||||
*******************************************************************************/
|
||||
public String key()
|
||||
{
|
||||
return ("crm-contact:" + contactId);
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.crm.api;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Idempotent activity-code initialization through normal table actions, never producer side effects.
|
||||
*******************************************************************************/
|
||||
public final class CrmStarterData
|
||||
{
|
||||
/*******************************************************************************
|
||||
** No instances.
|
||||
*******************************************************************************/
|
||||
private CrmStarterData()
|
||||
{
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Find or insert each supported activity code in the current instance.
|
||||
*******************************************************************************/
|
||||
public static void ensure() throws QException
|
||||
{
|
||||
ensure(CrmStore.asUser());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Initialize codes using the initiating operation's permission mode.
|
||||
*******************************************************************************/
|
||||
static void ensure(CrmStore store) throws QException
|
||||
{
|
||||
for(ActivityKind kind : ActivityKind.values())
|
||||
{
|
||||
if(!store.by("crm_activity_type", "code", kind.name()).isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String label = kind.name().substring(0, 1) + kind.name().substring(1).toLowerCase(Locale.ROOT).replace('_', ' ');
|
||||
List<QRecord> named = store.by("crm_activity_type", "name", label);
|
||||
if(!named.isEmpty() && named.get(0).getValue("code") == null)
|
||||
{
|
||||
store.update("crm_activity_type", new QRecord().withValue("id", named.get(0).getValue("id")).withValue("code", kind.name()));
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
store.insert("crm_activity_type", new QRecord().withValue("name", label).withValue("code", kind.name())
|
||||
.withValue("sortOrder", kind.ordinal()).withValue("isSystem", true));
|
||||
}
|
||||
catch(QException e)
|
||||
{
|
||||
if(store.by("crm_activity_type", "code", kind.name()).isEmpty())
|
||||
{
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.crm.api;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.crm.CrmSessionUtils;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionEnforcement;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Internal table-action boundary. Explicitly selects trusted host work or caller permission enforcement.
|
||||
*******************************************************************************/
|
||||
final class CrmStore
|
||||
{
|
||||
private final PermissionEnforcement permissionEnforcement;
|
||||
|
||||
/*******************************************************************************
|
||||
** Select permission enforcement for all actions in this store.
|
||||
*******************************************************************************/
|
||||
private CrmStore(PermissionEnforcement permissionEnforcement)
|
||||
{
|
||||
this.permissionEnforcement = permissionEnforcement;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Validated host intake is internal work, independent of the visitor's permissions.
|
||||
*******************************************************************************/
|
||||
static CrmStore trusted()
|
||||
{
|
||||
return (new CrmStore(PermissionEnforcement.SKIP));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Signed-in users and marketing workers must hold the permissions for their work.
|
||||
*******************************************************************************/
|
||||
static CrmStore asUser()
|
||||
{
|
||||
return (new CrmStore(PermissionEnforcement.ENFORCE));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Core requires an explicit reason when selecting SKIP for internal work.
|
||||
*******************************************************************************/
|
||||
private void applyPermissions(AbstractActionInput input)
|
||||
{
|
||||
if(permissionEnforcement == PermissionEnforcement.SKIP)
|
||||
{
|
||||
input.withPermissionBypass("CRM intake is trusted host work after public request validation");
|
||||
}
|
||||
else
|
||||
{
|
||||
input.withPermissionEnforcement(permissionEnforcement);
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Query through core actions.
|
||||
*******************************************************************************/
|
||||
List<QRecord> query(String table, QQueryFilter filter) throws QException
|
||||
{
|
||||
QueryInput input = new QueryInput(table).withFilter(filter);
|
||||
applyPermissions(input);
|
||||
return (new QueryAction().execute(input).getRecords());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Query a single field.
|
||||
*******************************************************************************/
|
||||
List<QRecord> by(String table, String field, Serializable value) throws QException
|
||||
{
|
||||
return (query(table, new QQueryFilter(new QFilterCriteria(field, QCriteriaOperator.EQUALS, value))));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Retrieve a visible record or fail without disclosing its contents.
|
||||
*******************************************************************************/
|
||||
QRecord get(String table, Integer id) throws QException
|
||||
{
|
||||
if(id == null)
|
||||
{
|
||||
throw (new QUserFacingException("A record identifier is required."));
|
||||
}
|
||||
List<QRecord> rows = by(table, "id", id);
|
||||
if(rows.isEmpty())
|
||||
{
|
||||
throw (new QUserFacingException("The CRM record is unavailable."));
|
||||
}
|
||||
return (rows.get(0));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Insert and surface record-level validation failures.
|
||||
*******************************************************************************/
|
||||
QRecord insert(String table, QRecord record) throws QException
|
||||
{
|
||||
InsertInput input = new InsertInput(table).withRecord(record);
|
||||
applyPermissions(input);
|
||||
return (checked(new InsertAction().execute(input).getRecords()));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Update without bypassing customizers, audit or locks.
|
||||
*******************************************************************************/
|
||||
QRecord update(String table, QRecord record) throws QException
|
||||
{
|
||||
UpdateInput input = new UpdateInput(table).withRecords(List.of(record));
|
||||
applyPermissions(input);
|
||||
return (checked(new UpdateAction().execute(input).getRecords()));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Record an interaction with the same permission mode as the initiating operation.
|
||||
*******************************************************************************/
|
||||
void recordActivity(Integer contactId, ActivityKind kind, String subject, String description) throws QException
|
||||
{
|
||||
get("crm_contact", contactId);
|
||||
if(kind == null || subject == null || subject.isBlank())
|
||||
{
|
||||
throw (new QUserFacingException("An activity kind and subject are required."));
|
||||
}
|
||||
CrmStarterData.ensure(this);
|
||||
Integer typeId = by("crm_activity_type", "code", kind.name()).get(0).getValueInteger("id");
|
||||
insert("crm_activity", new QRecord().withValue("contactId", contactId).withValue("activityTypeId", typeId)
|
||||
.withValue("subject", subject).withValue("description", description).withValue("ownerUserId", CrmSessionUtils.getCurrentUserId())
|
||||
.withValue("isCompleted", true).withValue("completedDate", Instant.now()));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Never return success when the action refused a record.
|
||||
*******************************************************************************/
|
||||
private static QRecord checked(List<QRecord> records) throws QException
|
||||
{
|
||||
if(records.isEmpty() || (records.get(0).getErrors() != null && !records.get(0).getErrors().isEmpty()))
|
||||
{
|
||||
throw (new QUserFacingException("CRM could not save the record. Check required fields and permissions."));
|
||||
}
|
||||
return (records.get(0));
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.crm.api;
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Explicit mailing-list signup and the exact wording accepted by the visitor.
|
||||
*******************************************************************************/
|
||||
public record CrmSubscribeRequest(String listKey, String email, String firstName, String lastName, String consentText, String source,
|
||||
String utmSource, String utmMedium, String utmCampaign, String utmContent, String utmTerm, String visitorKey, String clientHash)
|
||||
{
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.crm.api;
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Outcome of a mailing-list signup.
|
||||
*******************************************************************************/
|
||||
public record CrmSubscribeResult(SubscriptionStatus status)
|
||||
{
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.crm.api;
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Email eligibility with a stable reason code and no personal data.
|
||||
*******************************************************************************/
|
||||
public record Eligibility(boolean eligible, String reason)
|
||||
{
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.crm.api;
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Supported intake form kinds.
|
||||
*******************************************************************************/
|
||||
public enum FormType
|
||||
{
|
||||
CONTACT,
|
||||
NEWSLETTER,
|
||||
DEMO,
|
||||
OTHER
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.crm.api;
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** An active list offered as a marketing recipient scope.
|
||||
*******************************************************************************/
|
||||
public record RecipientScopeOption(String scope, String label)
|
||||
{
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.crm.api;
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Mailing-list subscription outcomes.
|
||||
*******************************************************************************/
|
||||
public enum SubscriptionStatus
|
||||
{
|
||||
PENDING_CONFIRMATION,
|
||||
SUBSCRIBED,
|
||||
ALREADY_SUBSCRIBED
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
/*******************************************************************************
|
||||
** POST_INSERT, POST_UPDATE, and POST_DELETE table customizer that creates
|
||||
** immutable audit log entries for tracked CRM entities.
|
||||
**
|
||||
** Registered globally on tracked tables (contact, company, deal, activity)
|
||||
** via MultiCustomizer in CrmQBitProducer.postProduceActions.
|
||||
*******************************************************************************/
|
||||
package com.kingsrook.qbits.crm.audit.customizers;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qbits.crm.audit.model.AuditLog;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmAuditAction;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmEntityType;
|
||||
import com.kingsrook.qbits.crm.CrmSessionUtils;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Audit log customizer that creates crm_audit_log entries for every insert,
|
||||
** update, and delete on tracked entities. For updates, field-level change
|
||||
** tracking captures old and new values, with special detection for owner
|
||||
** and pipeline stage changes.
|
||||
*******************************************************************************/
|
||||
public class CrmAuditLogCustomizer implements TableCustomizerInterface
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(CrmAuditLogCustomizer.class);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// map of table names to CrmEntityType IDs for tracked entities //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
private static final Map<String, Integer> TABLE_TO_ENTITY_TYPE = new HashMap<>();
|
||||
|
||||
static
|
||||
{
|
||||
TABLE_TO_ENTITY_TYPE.put("crmContact", CrmEntityType.CONTACT.getPossibleValueId());
|
||||
TABLE_TO_ENTITY_TYPE.put("crmCompany", CrmEntityType.COMPANY.getPossibleValueId());
|
||||
TABLE_TO_ENTITY_TYPE.put("crmDeal", CrmEntityType.DEAL.getPossibleValueId());
|
||||
TABLE_TO_ENTITY_TYPE.put("crmActivity", CrmEntityType.ACTIVITY.getPossibleValueId());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Post-insert: log a CREATED action for each inserted record.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> postInsert(InsertInput insertInput, List<QRecord> records) throws QException
|
||||
{
|
||||
String tableName = insertInput.getTableName();
|
||||
String userId = getCurrentUserId();
|
||||
|
||||
List<QRecord> auditRecords = new ArrayList<>();
|
||||
for(QRecord record : records)
|
||||
{
|
||||
if(record.getErrors() != null && !record.getErrors().isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Integer entityId = ValueUtils.getValueAsInteger(record.getValue("id"));
|
||||
if(entityId == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
auditRecords.add(buildAuditRecord(tableName, entityId, CrmAuditAction.CREATED, userId,
|
||||
null, null, null, "Record created"));
|
||||
}
|
||||
|
||||
insertAuditRecords(auditRecords);
|
||||
return (records);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Post-update: log an UPDATED action per changed field, with special
|
||||
** detection for OWNER_CHANGED and STAGE_CHANGED actions.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> postUpdate(UpdateInput updateInput, List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
|
||||
{
|
||||
String tableName = updateInput.getTableName();
|
||||
String userId = getCurrentUserId();
|
||||
|
||||
Optional<Map<Serializable, QRecord>> oldRecordMap = oldRecordListToMap(
|
||||
updateInput.getTable().getPrimaryKeyField(), oldRecordList);
|
||||
|
||||
if(oldRecordMap.isEmpty())
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// without old records, we cannot determine what changed -- skip logging //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
return (records);
|
||||
}
|
||||
|
||||
List<QRecord> auditRecords = new ArrayList<>();
|
||||
for(QRecord record : records)
|
||||
{
|
||||
if(record.getErrors() != null && !record.getErrors().isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Serializable primaryKey = record.getValue(updateInput.getTable().getPrimaryKeyField());
|
||||
Integer entityId = ValueUtils.getValueAsInteger(primaryKey);
|
||||
QRecord oldRecord = oldRecordMap.get().get(primaryKey);
|
||||
|
||||
if(entityId == null || oldRecord == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for(String fieldName : record.getValues().keySet())
|
||||
{
|
||||
Serializable oldVal = oldRecord.getValue(fieldName);
|
||||
Serializable newVal = record.getValue(fieldName);
|
||||
|
||||
if(!Objects.equals(oldVal, newVal))
|
||||
{
|
||||
String oldStr = oldVal == null ? null : String.valueOf(oldVal);
|
||||
String newStr = newVal == null ? null : String.valueOf(newVal);
|
||||
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// determine the specific action for special tracked fields //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
CrmAuditAction auditAction = CrmAuditAction.UPDATED;
|
||||
if("ownerUserId".equals(fieldName))
|
||||
{
|
||||
auditAction = CrmAuditAction.OWNER_CHANGED;
|
||||
}
|
||||
else if("pipelineStageId".equals(fieldName))
|
||||
{
|
||||
auditAction = CrmAuditAction.STAGE_CHANGED;
|
||||
}
|
||||
|
||||
auditRecords.add(buildAuditRecord(tableName, entityId, auditAction, userId,
|
||||
fieldName, oldStr, newStr, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
insertAuditRecords(auditRecords);
|
||||
return (records);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Post-delete: log a DELETED action for each deleted record.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> postDelete(DeleteInput deleteInput, List<QRecord> records) throws QException
|
||||
{
|
||||
String tableName = deleteInput.getTableName();
|
||||
String userId = getCurrentUserId();
|
||||
|
||||
List<QRecord> auditRecords = new ArrayList<>();
|
||||
for(QRecord record : records)
|
||||
{
|
||||
if(record.getErrors() != null && !record.getErrors().isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Integer entityId = ValueUtils.getValueAsInteger(record.getValue("id"));
|
||||
if(entityId == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
auditRecords.add(buildAuditRecord(tableName, entityId, CrmAuditAction.DELETED, userId,
|
||||
null, null, null, "Record deleted"));
|
||||
}
|
||||
|
||||
insertAuditRecords(auditRecords);
|
||||
return (records);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Build a single audit log QRecord.
|
||||
*******************************************************************************/
|
||||
private QRecord buildAuditRecord(String tableName, Integer entityId, CrmAuditAction action,
|
||||
String userId, String fieldName, String oldValue,
|
||||
String newValue, String message)
|
||||
{
|
||||
QRecord auditRecord = new QRecord();
|
||||
auditRecord.setValue("entityType", resolveEntityType(tableName));
|
||||
auditRecord.setValue("entityId", entityId);
|
||||
auditRecord.setValue("action", action.getPossibleValueId());
|
||||
auditRecord.setValue("userId", userId);
|
||||
|
||||
if(StringUtils.hasContent(fieldName))
|
||||
{
|
||||
auditRecord.setValue("fieldName", fieldName);
|
||||
}
|
||||
|
||||
if(oldValue != null)
|
||||
{
|
||||
auditRecord.setValue("oldValue", oldValue);
|
||||
}
|
||||
|
||||
if(newValue != null)
|
||||
{
|
||||
auditRecord.setValue("newValue", newValue);
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(message))
|
||||
{
|
||||
auditRecord.setValue("message", message);
|
||||
}
|
||||
|
||||
return (auditRecord);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Resolve a table name to its CrmEntityType integer ID.
|
||||
** Returns the mapped ID if known, or falls back to the table name string
|
||||
** for unrecognized tables (allowing forward-compatibility).
|
||||
*******************************************************************************/
|
||||
private Object resolveEntityType(String tableName)
|
||||
{
|
||||
Integer entityTypeId = TABLE_TO_ENTITY_TYPE.get(tableName);
|
||||
if(entityTypeId != null)
|
||||
{
|
||||
return (entityTypeId);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// fallback: use the table name directly for unrecognized entity types //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
return (tableName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Insert a batch of audit log records using InsertAction.
|
||||
*******************************************************************************/
|
||||
private void insertAuditRecords(List<QRecord> auditRecords) throws QException
|
||||
{
|
||||
if(auditRecords.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(AuditLog.TABLE_NAME);
|
||||
insertInput.setRecords(auditRecords);
|
||||
new InsertAction().execute(insertInput);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error inserting audit log records", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Get the current user ID from the QContext session.
|
||||
*******************************************************************************/
|
||||
private String getCurrentUserId()
|
||||
{
|
||||
return (CrmSessionUtils.getCurrentUserId());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,481 @@
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for the immutable CRM audit log table.
|
||||
*******************************************************************************/
|
||||
package com.kingsrook.qbits.crm.audit.model;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmAuditAction;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmEntityType;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreDeleteCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreUpdateCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Immutable append-only audit ledger for tracked CRM entities.
|
||||
** Records field-level changes, stage transitions, owner changes, and lifecycle
|
||||
** events with full before/after values and user attribution.
|
||||
*******************************************************************************/
|
||||
@QMetaDataProducingEntity(
|
||||
producePossibleValueSource = false,
|
||||
produceTableMetaData = true,
|
||||
tableMetaDataCustomizer = AuditLog.TableMetaDataCustomizer.class
|
||||
)
|
||||
public class AuditLog extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "crmAuditLog";
|
||||
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Integer id;
|
||||
|
||||
@QField(isRequired = true, possibleValueSourceName = CrmEntityType.NAME)
|
||||
private Integer entityType;
|
||||
|
||||
@QField(isRequired = true)
|
||||
private Integer entityId;
|
||||
|
||||
@QField(isRequired = true, possibleValueSourceName = CrmAuditAction.NAME)
|
||||
private Integer action;
|
||||
|
||||
@QField(isRequired = true)
|
||||
private String userId;
|
||||
|
||||
@QField(maxLength = 100)
|
||||
private String fieldName;
|
||||
|
||||
@QField()
|
||||
private String oldValue;
|
||||
|
||||
@QField()
|
||||
private String newValue;
|
||||
|
||||
@QField(maxLength = 1000)
|
||||
private String message;
|
||||
|
||||
@QField(isEditable = false)
|
||||
private Instant createDate;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** TableMetaDataCustomizer -- sets icon, record label, sections, and
|
||||
** registers PRE_UPDATE and PRE_DELETE customizers to enforce immutability.
|
||||
***************************************************************************/
|
||||
public static class TableMetaDataCustomizer implements MetaDataCustomizerInterface<QTableMetaData>
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public QTableMetaData customizeMetaData(QInstance qInstance, QTableMetaData table) throws QException
|
||||
{
|
||||
table
|
||||
.withIcon(new QIcon().withName("history"))
|
||||
.withRecordLabelFormat("%s %s #%s")
|
||||
.withRecordLabelFields("action", "entityType", "entityId")
|
||||
.withSection(SectionFactory.defaultT1("id", "entityType", "entityId", "action"))
|
||||
.withSection(new QFieldSection("details", new QIcon("text_snippet"), Tier.T2,
|
||||
List.of("userId", "fieldName", "oldValue", "newValue", "message")))
|
||||
.withSection(SectionFactory.defaultT3("createDate"));
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// register PRE_UPDATE and PRE_DELETE customizers to enforce immutability //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
table.withCustomizer(TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(PreventUpdateCustomizer.class));
|
||||
table.withCustomizer(TableCustomizers.PRE_DELETE_RECORD, new QCodeReference(PreventDeleteCustomizer.class));
|
||||
|
||||
return (table);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** PRE_UPDATE customizer that rejects all update attempts on audit log.
|
||||
***************************************************************************/
|
||||
public static class PreventUpdateCustomizer extends AbstractPreUpdateCustomizer
|
||||
{
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> apply(List<QRecord> records) throws QException
|
||||
{
|
||||
if("true".equals(QContext.getQSession().getValue("gdprBypass")))
|
||||
{
|
||||
return records;
|
||||
}
|
||||
throw new QUserFacingException("Audit log records cannot be modified or deleted");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** PRE_DELETE customizer that rejects all delete attempts on audit log.
|
||||
***************************************************************************/
|
||||
public static class PreventDeleteCustomizer extends AbstractPreDeleteCustomizer
|
||||
{
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> apply(List<QRecord> records) throws QException
|
||||
{
|
||||
if("true".equals(QContext.getQSession().getValue("gdprBypass")))
|
||||
{
|
||||
return records;
|
||||
}
|
||||
throw new QUserFacingException("Audit log records cannot be modified or deleted");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Default constructor
|
||||
*******************************************************************************/
|
||||
public AuditLog()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor that takes a QRecord
|
||||
*******************************************************************************/
|
||||
public AuditLog(QRecord record)
|
||||
{
|
||||
populateFromQRecord(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for id
|
||||
*******************************************************************************/
|
||||
public Integer getId()
|
||||
{
|
||||
return (this.id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for id
|
||||
*******************************************************************************/
|
||||
public void setId(Integer id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for id
|
||||
*******************************************************************************/
|
||||
public AuditLog withId(Integer id)
|
||||
{
|
||||
this.id = id;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for entityType
|
||||
*******************************************************************************/
|
||||
public Integer getEntityType()
|
||||
{
|
||||
return (this.entityType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for entityType
|
||||
*******************************************************************************/
|
||||
public void setEntityType(Integer entityType)
|
||||
{
|
||||
this.entityType = entityType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for entityType
|
||||
*******************************************************************************/
|
||||
public AuditLog withEntityType(Integer entityType)
|
||||
{
|
||||
this.entityType = entityType;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for entityId
|
||||
*******************************************************************************/
|
||||
public Integer getEntityId()
|
||||
{
|
||||
return (this.entityId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for entityId
|
||||
*******************************************************************************/
|
||||
public void setEntityId(Integer entityId)
|
||||
{
|
||||
this.entityId = entityId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for entityId
|
||||
*******************************************************************************/
|
||||
public AuditLog withEntityId(Integer entityId)
|
||||
{
|
||||
this.entityId = entityId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for action
|
||||
*******************************************************************************/
|
||||
public Integer getAction()
|
||||
{
|
||||
return (this.action);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for action
|
||||
*******************************************************************************/
|
||||
public void setAction(Integer action)
|
||||
{
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for action
|
||||
*******************************************************************************/
|
||||
public AuditLog withAction(Integer action)
|
||||
{
|
||||
this.action = action;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for userId
|
||||
*******************************************************************************/
|
||||
public String getUserId()
|
||||
{
|
||||
return (this.userId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for userId
|
||||
*******************************************************************************/
|
||||
public void setUserId(String userId)
|
||||
{
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for userId
|
||||
*******************************************************************************/
|
||||
public AuditLog withUserId(String userId)
|
||||
{
|
||||
this.userId = userId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for fieldName
|
||||
*******************************************************************************/
|
||||
public String getFieldName()
|
||||
{
|
||||
return (this.fieldName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for fieldName
|
||||
*******************************************************************************/
|
||||
public void setFieldName(String fieldName)
|
||||
{
|
||||
this.fieldName = fieldName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for fieldName
|
||||
*******************************************************************************/
|
||||
public AuditLog withFieldName(String fieldName)
|
||||
{
|
||||
this.fieldName = fieldName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for oldValue
|
||||
*******************************************************************************/
|
||||
public String getOldValue()
|
||||
{
|
||||
return (this.oldValue);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for oldValue
|
||||
*******************************************************************************/
|
||||
public void setOldValue(String oldValue)
|
||||
{
|
||||
this.oldValue = oldValue;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for oldValue
|
||||
*******************************************************************************/
|
||||
public AuditLog withOldValue(String oldValue)
|
||||
{
|
||||
this.oldValue = oldValue;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for newValue
|
||||
*******************************************************************************/
|
||||
public String getNewValue()
|
||||
{
|
||||
return (this.newValue);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for newValue
|
||||
*******************************************************************************/
|
||||
public void setNewValue(String newValue)
|
||||
{
|
||||
this.newValue = newValue;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for newValue
|
||||
*******************************************************************************/
|
||||
public AuditLog withNewValue(String newValue)
|
||||
{
|
||||
this.newValue = newValue;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for message
|
||||
*******************************************************************************/
|
||||
public String getMessage()
|
||||
{
|
||||
return (this.message);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for message
|
||||
*******************************************************************************/
|
||||
public void setMessage(String message)
|
||||
{
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for message
|
||||
*******************************************************************************/
|
||||
public AuditLog withMessage(String message)
|
||||
{
|
||||
this.message = message;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for createDate
|
||||
*******************************************************************************/
|
||||
public Instant getCreateDate()
|
||||
{
|
||||
return (this.createDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for createDate
|
||||
*******************************************************************************/
|
||||
public void setCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for createDate
|
||||
*******************************************************************************/
|
||||
public AuditLog withCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.crm.bulkload;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.kingsrook.qbits.crm.core.CrmRecords;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.savedbulkloadprofiles.SavedBulkLoadProfile;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadProfile;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Starter mappings for the host's core bulk loader. The host owns its tables.
|
||||
*******************************************************************************/
|
||||
public final class CrmBulkLoadProfiles
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private CrmBulkLoadProfiles()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Call once during host startup, after core profile tables are installed.
|
||||
** Existing profiles, including user edits, are preserved by label.
|
||||
*******************************************************************************/
|
||||
public static void ensure() throws QException
|
||||
{
|
||||
if(QContext.getQInstance().getTable(SavedBulkLoadProfile.TABLE_NAME) == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try(InputStream stream = CrmBulkLoadProfiles.class.getResourceAsStream("/bulkload/crm-profiles.json"))
|
||||
{
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
for(JsonNode profile : mapper.readTree(stream))
|
||||
{
|
||||
String label = profile.get("label").asText();
|
||||
if(!new QueryAction().execute(new QueryInput(SavedBulkLoadProfile.TABLE_NAME).withFilter(new QQueryFilter(
|
||||
new QFilterCriteria("label", QCriteriaOperator.EQUALS, label)).withLimit(1))).getRecords().isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
BulkLoadProfile mapping = mapper.treeToValue(profile.get("mapping"), BulkLoadProfile.class);
|
||||
CrmRecords.insert(SavedBulkLoadProfile.TABLE_NAME, new QRecord().withValue("label", label)
|
||||
.withValue("tableName", profile.get("tableName").asText()).withValue("isBulkEdit", false)
|
||||
.withValue("mappingJson", mapper.writeValueAsString(mapping)));
|
||||
}
|
||||
}
|
||||
catch(IOException | IllegalArgumentException e)
|
||||
{
|
||||
throw (new QException("Could not load CRM bulk-load profiles", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.crm.core;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Checked writes for background work: record errors must not advance checkpoints.
|
||||
*******************************************************************************/
|
||||
public final class CrmRecords
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private CrmRecords()
|
||||
{
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Insert and require a successful persisted row.
|
||||
*******************************************************************************/
|
||||
public static QRecord insert(String table, QRecord row) throws QException
|
||||
{
|
||||
return (check(new InsertAction().execute(new InsertInput(table).withRecord(row)).getRecords()));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Update and require a successful persisted row.
|
||||
*******************************************************************************/
|
||||
public static QRecord update(String table, QRecord row) throws QException
|
||||
{
|
||||
return (check(new UpdateAction().execute(new UpdateInput(table).withRecord(row)).getRecords()));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** DML errors are failed work, never successful job progress.
|
||||
*******************************************************************************/
|
||||
private static QRecord check(List<QRecord> rows) throws QException
|
||||
{
|
||||
if(rows.isEmpty() || rows.stream().anyMatch(row -> row.getErrors() != null && !row.getErrors().isEmpty()))
|
||||
{
|
||||
throw (new QException("CRM write was rejected"));
|
||||
}
|
||||
return (rows.getFirst());
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package com.kingsrook.qbits.crm.core.customizers;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qbits.crm.core.model.Contact;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.FieldSecurityLockHelper;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** RC.6 recordLabelFormat is printf-only; preserve a computed query/get label.
|
||||
*******************************************************************************/
|
||||
public class ContactLabelCustomizer extends AbstractPostQueryCustomizer
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Keep missing names absent in storage while giving records useful labels.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> apply(List<QRecord> records)
|
||||
{
|
||||
for(QRecord record : records)
|
||||
{
|
||||
QRecord visible = new QRecord().withValue("id", record.getValue("id"));
|
||||
for(String name : List.of("firstName", "lastName", "email"))
|
||||
{
|
||||
var field = QContext.getQInstance().getTable(Contact.TABLE_NAME).getField(name);
|
||||
if(FieldSecurityLockHelper.isFieldAccessible(field)
|
||||
&& !(input != null && input.getShouldOmitHiddenFields() && field.getIsHidden())
|
||||
&& !(input != null && input.getShouldMaskPasswords() && field.getType().needsMasked()))
|
||||
{
|
||||
visible.setValue(name, record.getValue(name));
|
||||
}
|
||||
}
|
||||
record.setRecordLabel(label(visible));
|
||||
}
|
||||
return (records);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Prefer the supplied name, then email, then the record identifier.
|
||||
*******************************************************************************/
|
||||
public static String label(QRecord record)
|
||||
{
|
||||
String name = (Objects.toString(record.getValue("firstName"), "").strip() + " "
|
||||
+ Objects.toString(record.getValue("lastName"), "").strip()).strip();
|
||||
return (name.isBlank() ? Objects.toString(record.getValue("email"), Objects.toString(record.getValue("id"), "Contact")) : name);
|
||||
}
|
||||
}
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.customizers;
|
||||
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreInsertCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreUpdateCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Stamps crm_currency.rateLastUpdatedDate whenever a rate is set or changed, so
|
||||
** the date says when the exchange rate was last touched. Editing only the name,
|
||||
** symbol or active flag leaves it alone.
|
||||
*******************************************************************************/
|
||||
public final class CurrencyRateDateCustomizer
|
||||
{
|
||||
public static final String RATE_FIELD = "exchangeRateToBase";
|
||||
public static final String DATE_FIELD = "rateLastUpdatedDate";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private CurrencyRateDateCustomizer()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A new currency has a rate from the moment it exists.
|
||||
*******************************************************************************/
|
||||
public static class OnInsert extends AbstractPreInsertCustomizer
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> apply(List<QRecord> records) throws QException
|
||||
{
|
||||
Instant now = Instant.now();
|
||||
for(QRecord record : records)
|
||||
{
|
||||
record.setValue(DATE_FIELD, now);
|
||||
}
|
||||
return (records);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** An update that carries a different rate than the stored one moves the date.
|
||||
*******************************************************************************/
|
||||
public static class OnUpdate extends AbstractPreUpdateCustomizer
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> apply(List<QRecord> records) throws QException
|
||||
{
|
||||
if(isPreview || oldRecordList == null)
|
||||
{
|
||||
return (records);
|
||||
}
|
||||
|
||||
Instant now = Instant.now();
|
||||
for(QRecord record : records)
|
||||
{
|
||||
QRecord oldRecord = getOldRecordMap().get(record.getValue("id"));
|
||||
if(oldRecord != null && record.getValues().containsKey(RATE_FIELD)
|
||||
&& !sameRate(ValueUtils.getValueAsBigDecimal(record.getValue(RATE_FIELD)), ValueUtils.getValueAsBigDecimal(oldRecord.getValue(RATE_FIELD))))
|
||||
{
|
||||
record.setValue(DATE_FIELD, now);
|
||||
}
|
||||
}
|
||||
return (records);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Numeric equality, so 1 and 1.000000 are the same rate.
|
||||
*******************************************************************************/
|
||||
static boolean sameRate(BigDecimal a, BigDecimal b)
|
||||
{
|
||||
return (a == null ? b == null : b != null && a.compareTo(b) == 0);
|
||||
}
|
||||
|
||||
}
|
||||
-22
@@ -1,29 +1,7 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** POST_INSERT customizer for form submissions. Auto-links contactId if a
|
||||
** contact with a matching email address already exists.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.customizers;
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for Attachment table
|
||||
*******************************************************************************/
|
||||
package com.kingsrook.qbits.crm.core.model;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmEntityType;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for Attachment table -- polymorphic file attachments linked
|
||||
** to any CRM entity via entityType + entityId. Insert-and-delete-only; no
|
||||
** modifyDate. Carries denormalized clientId for security lock.
|
||||
*******************************************************************************/
|
||||
@QMetaDataProducingEntity(
|
||||
produceTableMetaData = true,
|
||||
tableMetaDataCustomizer = Attachment.TableMetaDataCustomizer.class
|
||||
)
|
||||
public class Attachment extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "crmAttachment";
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static class TableMetaDataCustomizer implements MetaDataCustomizerInterface<QTableMetaData>
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public QTableMetaData customizeMetaData(QInstance qInstance, QTableMetaData table) throws QException
|
||||
{
|
||||
table
|
||||
.withIcon(new QIcon().withName("attach_file"))
|
||||
.withRecordLabelFormat("%s")
|
||||
.withRecordLabelFields("fileName")
|
||||
.withSection(SectionFactory.defaultT1("id", "entityType", "entityId", "fileName"))
|
||||
.withSection(SectionFactory.defaultT2("fileType", "fileSizeBytes", "storageReference", "uploadedByUserId"))
|
||||
.withSection(SectionFactory.defaultT3("createDate"));
|
||||
|
||||
return (table);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Integer id;
|
||||
|
||||
@QField(isRequired = true, possibleValueSourceName = CrmEntityType.NAME)
|
||||
private Integer entityType;
|
||||
|
||||
@QField(isRequired = true)
|
||||
private Integer entityId;
|
||||
|
||||
@QField(isRequired = true, maxLength = 500, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String fileName;
|
||||
|
||||
@QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String fileType;
|
||||
|
||||
@QField()
|
||||
private Long fileSizeBytes;
|
||||
|
||||
@QField(isRequired = true, maxLength = 1000, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String storageReference;
|
||||
|
||||
@QField(isRequired = true)
|
||||
private String uploadedByUserId;
|
||||
|
||||
@QField(isEditable = false)
|
||||
private Instant createDate;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Default constructor
|
||||
*******************************************************************************/
|
||||
public Attachment()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor that takes a QRecord
|
||||
*******************************************************************************/
|
||||
public Attachment(QRecord record)
|
||||
{
|
||||
populateFromQRecord(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for id
|
||||
*******************************************************************************/
|
||||
public Integer getId()
|
||||
{
|
||||
return (this.id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for id
|
||||
*******************************************************************************/
|
||||
public void setId(Integer id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for id
|
||||
*******************************************************************************/
|
||||
public Attachment withId(Integer id)
|
||||
{
|
||||
this.id = id;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for entityType
|
||||
*******************************************************************************/
|
||||
public Integer getEntityType()
|
||||
{
|
||||
return (this.entityType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for entityType
|
||||
*******************************************************************************/
|
||||
public void setEntityType(Integer entityType)
|
||||
{
|
||||
this.entityType = entityType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for entityType
|
||||
*******************************************************************************/
|
||||
public Attachment withEntityType(Integer entityType)
|
||||
{
|
||||
this.entityType = entityType;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for entityId
|
||||
*******************************************************************************/
|
||||
public Integer getEntityId()
|
||||
{
|
||||
return (this.entityId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for entityId
|
||||
*******************************************************************************/
|
||||
public void setEntityId(Integer entityId)
|
||||
{
|
||||
this.entityId = entityId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for entityId
|
||||
*******************************************************************************/
|
||||
public Attachment withEntityId(Integer entityId)
|
||||
{
|
||||
this.entityId = entityId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for fileName
|
||||
*******************************************************************************/
|
||||
public String getFileName()
|
||||
{
|
||||
return (this.fileName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for fileName
|
||||
*******************************************************************************/
|
||||
public void setFileName(String fileName)
|
||||
{
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for fileName
|
||||
*******************************************************************************/
|
||||
public Attachment withFileName(String fileName)
|
||||
{
|
||||
this.fileName = fileName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for fileType
|
||||
*******************************************************************************/
|
||||
public String getFileType()
|
||||
{
|
||||
return (this.fileType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for fileType
|
||||
*******************************************************************************/
|
||||
public void setFileType(String fileType)
|
||||
{
|
||||
this.fileType = fileType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for fileType
|
||||
*******************************************************************************/
|
||||
public Attachment withFileType(String fileType)
|
||||
{
|
||||
this.fileType = fileType;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for fileSizeBytes
|
||||
*******************************************************************************/
|
||||
public Long getFileSizeBytes()
|
||||
{
|
||||
return (this.fileSizeBytes);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for fileSizeBytes
|
||||
*******************************************************************************/
|
||||
public void setFileSizeBytes(Long fileSizeBytes)
|
||||
{
|
||||
this.fileSizeBytes = fileSizeBytes;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for fileSizeBytes
|
||||
*******************************************************************************/
|
||||
public Attachment withFileSizeBytes(Long fileSizeBytes)
|
||||
{
|
||||
this.fileSizeBytes = fileSizeBytes;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for storageReference
|
||||
*******************************************************************************/
|
||||
public String getStorageReference()
|
||||
{
|
||||
return (this.storageReference);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for storageReference
|
||||
*******************************************************************************/
|
||||
public void setStorageReference(String storageReference)
|
||||
{
|
||||
this.storageReference = storageReference;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for storageReference
|
||||
*******************************************************************************/
|
||||
public Attachment withStorageReference(String storageReference)
|
||||
{
|
||||
this.storageReference = storageReference;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for uploadedByUserId
|
||||
*******************************************************************************/
|
||||
public String getUploadedByUserId()
|
||||
{
|
||||
return (this.uploadedByUserId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for uploadedByUserId
|
||||
*******************************************************************************/
|
||||
public void setUploadedByUserId(String uploadedByUserId)
|
||||
{
|
||||
this.uploadedByUserId = uploadedByUserId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for uploadedByUserId
|
||||
*******************************************************************************/
|
||||
public Attachment withUploadedByUserId(String uploadedByUserId)
|
||||
{
|
||||
this.uploadedByUserId = uploadedByUserId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for createDate
|
||||
*******************************************************************************/
|
||||
public Instant getCreateDate()
|
||||
{
|
||||
return (this.createDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for createDate
|
||||
*******************************************************************************/
|
||||
public void setCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for createDate
|
||||
*******************************************************************************/
|
||||
public Attachment withCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,35 +1,13 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for Company table
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model;
|
||||
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.crm.activities.model.Activity;
|
||||
import com.kingsrook.qbits.crm.CrmQBitConfig;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmCompanyType;
|
||||
import com.kingsrook.qbits.crm.deals.model.Deal;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
@@ -45,10 +23,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.Child
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildRecordListWidget;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildTable;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitProductionContext;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||
import com.kingsrook.qqq.backend.core.utils.collections.MutableList;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -60,20 +41,20 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||
produceTableMetaData = true,
|
||||
tableMetaDataCustomizer = Company.TableMetaDataCustomizer.class,
|
||||
childTables = {
|
||||
@ChildTable(childTableEntityClass = Activity.class, joinFieldName = "companyId",
|
||||
childJoin = @ChildJoin(enabled = true),
|
||||
childRecordListWidget = @ChildRecordListWidget(label = "Activities", enabled = true, maxRows = 25)),
|
||||
@ChildTable(childTableEntityClass = Contact.class, joinFieldName = "companyId",
|
||||
childJoin = @ChildJoin(enabled = true),
|
||||
childRecordListWidget = @ChildRecordListWidget(label = "Contacts", enabled = true, maxRows = 50)),
|
||||
@ChildTable(childTableEntityClass = Deal.class, joinFieldName = "companyId",
|
||||
childJoin = @ChildJoin(enabled = true),
|
||||
childRecordListWidget = @ChildRecordListWidget(label = "Deals", enabled = true, maxRows = 25))
|
||||
childRecordListWidget = @ChildRecordListWidget(label = "Deals", enabled = true, maxRows = 25)),
|
||||
@ChildTable(childTableEntityClass = CompanyTag.class, joinFieldName = "companyId",
|
||||
childJoin = @ChildJoin(enabled = true),
|
||||
childRecordListWidget = @ChildRecordListWidget(label = "Tags", enabled = true, maxRows = 50))
|
||||
}
|
||||
)
|
||||
public class Company extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "crm_company";
|
||||
public static final String TABLE_NAME = "crmCompany";
|
||||
|
||||
|
||||
|
||||
@@ -104,7 +85,30 @@ public class Company extends QRecordEntity
|
||||
.withSection(SectionFactory.defaultT2("description"))
|
||||
.withSection(SectionFactory.defaultT3("lastActivityDate", "lastContactedDate", "createDate", "modifyDate"));
|
||||
|
||||
///////////////////////////////////////////////////////////////
|
||||
// inject security fields and locks from CrmQBitConfig //
|
||||
///////////////////////////////////////////////////////////////
|
||||
CrmQBitConfig config = (CrmQBitConfig) QBitProductionContext.peekQBitConfig();
|
||||
if(config != null)
|
||||
{
|
||||
if(config.getSecurityFields() != null)
|
||||
{
|
||||
t1Section.setFieldNames(new MutableList<>(t1Section.getFieldNames()));
|
||||
for(QFieldMetaData fieldMetaData : config.getSecurityFields())
|
||||
{
|
||||
table.addField(fieldMetaData.clone());
|
||||
t1Section.getFieldNames().add(fieldMetaData.getName());
|
||||
}
|
||||
}
|
||||
|
||||
if(config.getRecordSecurityLocks() != null)
|
||||
{
|
||||
for(RecordSecurityLock lock : config.getRecordSecurityLocks())
|
||||
{
|
||||
table.withRecordSecurityLock(lock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (table);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for CompanyTag table
|
||||
*******************************************************************************/
|
||||
package com.kingsrook.qbits.crm.core.model;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for CompanyTag junction table -- many-to-many link between
|
||||
** companies and tags.
|
||||
*******************************************************************************/
|
||||
@QMetaDataProducingEntity(
|
||||
produceTableMetaData = true,
|
||||
tableMetaDataCustomizer = CompanyTag.TableMetaDataCustomizer.class
|
||||
)
|
||||
public class CompanyTag extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "crmCompanyTag";
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static class TableMetaDataCustomizer implements MetaDataCustomizerInterface<QTableMetaData>
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public QTableMetaData customizeMetaData(QInstance qInstance, QTableMetaData table) throws QException
|
||||
{
|
||||
table
|
||||
.withIcon(new QIcon().withName("label"))
|
||||
.withRecordLabelFormat("Company %s - Tag %s")
|
||||
.withRecordLabelFields("companyId", "tagId")
|
||||
.withUniqueKey(new UniqueKey("companyId", "tagId"))
|
||||
.withSection(SectionFactory.defaultT1("id", "companyId", "tagId"))
|
||||
.withSection(SectionFactory.defaultT3("createDate"));
|
||||
|
||||
return (table);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Integer id;
|
||||
|
||||
@QField(isRequired = true, possibleValueSourceName = Company.TABLE_NAME)
|
||||
private Integer companyId;
|
||||
|
||||
@QField(isRequired = true, possibleValueSourceName = Tag.TABLE_NAME)
|
||||
private Integer tagId;
|
||||
|
||||
@QField(isEditable = false)
|
||||
private Instant createDate;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Default constructor
|
||||
*******************************************************************************/
|
||||
public CompanyTag()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor that takes a QRecord
|
||||
*******************************************************************************/
|
||||
public CompanyTag(QRecord record)
|
||||
{
|
||||
populateFromQRecord(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for id
|
||||
*******************************************************************************/
|
||||
public Integer getId()
|
||||
{
|
||||
return (this.id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for id
|
||||
*******************************************************************************/
|
||||
public void setId(Integer id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for id
|
||||
*******************************************************************************/
|
||||
public CompanyTag withId(Integer id)
|
||||
{
|
||||
this.id = id;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for companyId
|
||||
*******************************************************************************/
|
||||
public Integer getCompanyId()
|
||||
{
|
||||
return (this.companyId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for companyId
|
||||
*******************************************************************************/
|
||||
public void setCompanyId(Integer companyId)
|
||||
{
|
||||
this.companyId = companyId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for companyId
|
||||
*******************************************************************************/
|
||||
public CompanyTag withCompanyId(Integer companyId)
|
||||
{
|
||||
this.companyId = companyId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for tagId
|
||||
*******************************************************************************/
|
||||
public Integer getTagId()
|
||||
{
|
||||
return (this.tagId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for tagId
|
||||
*******************************************************************************/
|
||||
public void setTagId(Integer tagId)
|
||||
{
|
||||
this.tagId = tagId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for tagId
|
||||
*******************************************************************************/
|
||||
public CompanyTag withTagId(Integer tagId)
|
||||
{
|
||||
this.tagId = tagId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for createDate
|
||||
*******************************************************************************/
|
||||
public Instant getCreateDate()
|
||||
{
|
||||
return (this.createDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for createDate
|
||||
*******************************************************************************/
|
||||
public void setCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for createDate
|
||||
*******************************************************************************/
|
||||
public CompanyTag withCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,40 +1,14 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for ConsentRecord table
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmConsentStatus;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmConsentType;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmLegalBasis;
|
||||
import com.kingsrook.qbits.crm.privacy.CrmPersonalData.ErasureFlag;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.AiFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.DataClassification;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreDeleteCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreUpdateCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
@@ -63,7 +37,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory;
|
||||
)
|
||||
public class ConsentRecord extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "crm_consent_record";
|
||||
public static final String TABLE_NAME = "crmConsentRecord";
|
||||
|
||||
|
||||
|
||||
@@ -85,7 +59,7 @@ public class ConsentRecord extends QRecordEntity
|
||||
.withRecordLabelFormat("Consent %s - %s")
|
||||
.withRecordLabelFields("contactId", "consentType")
|
||||
.withSection(SectionFactory.defaultT1("id", "contactId", "consentType", "status"))
|
||||
.withSection(SectionFactory.defaultT2("legalBasis", "source", "consentDate", "expiryDate", "consentText", "evidenceFormSubmissionId"))
|
||||
.withSection(SectionFactory.defaultT2("legalBasis", "source", "consentDate", "expiryDate", "ipAddress"))
|
||||
.withSection(SectionFactory.defaultT3("createDate"));
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
@@ -94,8 +68,6 @@ public class ConsentRecord extends QRecordEntity
|
||||
table.withCustomizer(TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(PreventUpdateCustomizer.class));
|
||||
table.withCustomizer(TableCustomizers.PRE_DELETE_RECORD, new QCodeReference(PreventDeleteCustomizer.class));
|
||||
|
||||
table.getField("consentText").withSupplementalMetaData(new AiFieldMetaData().withClassification(DataClassification.PII));
|
||||
|
||||
return (table);
|
||||
}
|
||||
}
|
||||
@@ -113,12 +85,6 @@ public class ConsentRecord extends QRecordEntity
|
||||
@Override
|
||||
public List<QRecord> apply(List<QRecord> records) throws QException
|
||||
{
|
||||
if(updateInput != null && updateInput.hasFlag(ErasureFlag.REDACT_CONSENT)
|
||||
&& records.stream().allMatch(row -> Set.of("id", "consentText").containsAll(row.getValues().keySet())
|
||||
&& "erased".equals(row.getValueString("consentText"))))
|
||||
{
|
||||
return (records);
|
||||
}
|
||||
throw new QUserFacingException("Consent records cannot be modified or deleted");
|
||||
}
|
||||
}
|
||||
@@ -142,12 +108,6 @@ public class ConsentRecord extends QRecordEntity
|
||||
|
||||
|
||||
|
||||
@QField()
|
||||
private String consentText;
|
||||
|
||||
@QField(possibleValueSourceName = FormSubmission.TABLE_NAME)
|
||||
private Integer evidenceFormSubmissionId;
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Integer id;
|
||||
|
||||
@@ -172,6 +132,9 @@ public class ConsentRecord extends QRecordEntity
|
||||
@QField()
|
||||
private Instant expiryDate;
|
||||
|
||||
@QField(maxLength = 45, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String ipAddress;
|
||||
|
||||
@QField(isEditable = false)
|
||||
private Instant createDate;
|
||||
|
||||
@@ -444,6 +407,37 @@ public class ConsentRecord extends QRecordEntity
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for ipAddress
|
||||
*******************************************************************************/
|
||||
public String getIpAddress()
|
||||
{
|
||||
return (this.ipAddress);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for ipAddress
|
||||
*******************************************************************************/
|
||||
public void setIpAddress(String ipAddress)
|
||||
{
|
||||
this.ipAddress = ipAddress;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for ipAddress
|
||||
*******************************************************************************/
|
||||
public ConsentRecord withIpAddress(String ipAddress)
|
||||
{
|
||||
this.ipAddress = ipAddress;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for createDate
|
||||
*******************************************************************************/
|
||||
@@ -473,61 +467,4 @@ public class ConsentRecord extends QRecordEntity
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for consentText
|
||||
*******************************************************************************/
|
||||
public String getConsentText()
|
||||
{
|
||||
return (this.consentText);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for consentText
|
||||
*******************************************************************************/
|
||||
public void setConsentText(String consentText)
|
||||
{
|
||||
this.consentText = consentText;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for consentText
|
||||
*******************************************************************************/
|
||||
public ConsentRecord withConsentText(String consentText)
|
||||
{
|
||||
this.consentText = consentText;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for evidenceFormSubmissionId
|
||||
*******************************************************************************/
|
||||
public Integer getEvidenceFormSubmissionId()
|
||||
{
|
||||
return (this.evidenceFormSubmissionId);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for evidenceFormSubmissionId
|
||||
*******************************************************************************/
|
||||
public void setEvidenceFormSubmissionId(Integer evidenceFormSubmissionId)
|
||||
{
|
||||
this.evidenceFormSubmissionId = evidenceFormSubmissionId;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for evidenceFormSubmissionId
|
||||
*******************************************************************************/
|
||||
public ConsentRecord withEvidenceFormSubmissionId(Integer evidenceFormSubmissionId)
|
||||
{
|
||||
this.evidenceFormSubmissionId = evidenceFormSubmissionId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,37 +1,14 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for Contact table
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.crm.CrmQBitConfig;
|
||||
import com.kingsrook.qbits.crm.activities.model.Activity;
|
||||
import com.kingsrook.qbits.crm.deals.model.Deal;
|
||||
import com.kingsrook.qbits.crm.email.model.EmailMessage;
|
||||
import com.kingsrook.qbits.crm.email.model.SequenceEnrollment;
|
||||
import com.kingsrook.qbits.crm.deals.model.DealContact;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
@@ -45,39 +22,33 @@ import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.Child
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildRecordListWidget;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildTable;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitProductionContext;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||
import com.kingsrook.qqq.backend.core.utils.collections.MutableList;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for Contact table -- the primary person record in the CRM.
|
||||
** Contacts are linked to companies, activities and deals.
|
||||
** Contacts are linked to companies, activities, deals, and tags.
|
||||
*******************************************************************************/
|
||||
@QMetaDataProducingEntity(
|
||||
producePossibleValueSource = true,
|
||||
produceTableMetaData = true,
|
||||
tableMetaDataCustomizer = Contact.TableMetaDataCustomizer.class,
|
||||
childTables = {
|
||||
@ChildTable(childTableEntityClass = Deal.class, joinFieldName = "primaryContactId",
|
||||
childJoin = @ChildJoin(enabled = true),
|
||||
childRecordListWidget = @ChildRecordListWidget(label = "Deals", enabled = true, maxRows = 25)),
|
||||
@ChildTable(childTableEntityClass = EmailListMember.class, joinFieldName = "contactId",
|
||||
childJoin = @ChildJoin(enabled = true),
|
||||
childRecordListWidget = @ChildRecordListWidget(label = "List Memberships", enabled = true, maxRows = 25)),
|
||||
@ChildTable(childTableEntityClass = SequenceEnrollment.class, joinFieldName = "contactId",
|
||||
childJoin = @ChildJoin(enabled = true),
|
||||
childRecordListWidget = @ChildRecordListWidget(label = "Enrollments", enabled = true, maxRows = 25)),
|
||||
@ChildTable(childTableEntityClass = EmailMessage.class, joinFieldName = "contactId",
|
||||
childJoin = @ChildJoin(enabled = true),
|
||||
childRecordListWidget = @ChildRecordListWidget(label = "Email Messages", enabled = true, maxRows = 25)),
|
||||
@ChildTable(childTableEntityClass = FormSubmission.class, joinFieldName = "contactId",
|
||||
childJoin = @ChildJoin(enabled = true),
|
||||
childRecordListWidget = @ChildRecordListWidget(label = "Web Leads", enabled = true, maxRows = 25)),
|
||||
@ChildTable(childTableEntityClass = Activity.class, joinFieldName = "contactId",
|
||||
childJoin = @ChildJoin(enabled = true),
|
||||
childRecordListWidget = @ChildRecordListWidget(label = "Activities", enabled = true, maxRows = 50)),
|
||||
@ChildTable(childTableEntityClass = DealContact.class, joinFieldName = "contactId",
|
||||
childJoin = @ChildJoin(enabled = true),
|
||||
childRecordListWidget = @ChildRecordListWidget(label = "Deals", enabled = true, maxRows = 25)),
|
||||
@ChildTable(childTableEntityClass = ContactTag.class, joinFieldName = "contactId",
|
||||
childJoin = @ChildJoin(enabled = true),
|
||||
childRecordListWidget = @ChildRecordListWidget(label = "Tags", enabled = true, maxRows = 50)),
|
||||
@ChildTable(childTableEntityClass = ConsentRecord.class, joinFieldName = "contactId",
|
||||
childJoin = @ChildJoin(enabled = true),
|
||||
childRecordListWidget = @ChildRecordListWidget(label = "Consent Records", enabled = true, maxRows = 25))
|
||||
@@ -85,7 +56,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||
)
|
||||
public class Contact extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "crm_contact";
|
||||
public static final String TABLE_NAME = "crmContact";
|
||||
|
||||
|
||||
|
||||
@@ -101,25 +72,49 @@ public class Contact extends QRecordEntity
|
||||
@Override
|
||||
public QTableMetaData customizeMetaData(QInstance qInstance, QTableMetaData table) throws QException
|
||||
{
|
||||
QFieldSection t1Section = SectionFactory.defaultT1("firstName", "lastName", "email", "phone");
|
||||
QFieldSection t1Section = SectionFactory.defaultT1("id", "firstName", "lastName", "email", "phone");
|
||||
|
||||
table
|
||||
.withIcon(new QIcon().withName("person"))
|
||||
.withRecordLabelFormat(null)
|
||||
.withRecordLabelFields(List.of())
|
||||
.withRecordLabelFormat("%s %s")
|
||||
.withRecordLabelFields("firstName", "lastName")
|
||||
.withSection(t1Section)
|
||||
.withSection(new QFieldSection("details", new QIcon().withName("info"), Tier.T2,
|
||||
List.of("companyId", "jobTitle", "department", "lifecycleStageId", "leadSourceId",
|
||||
"ownerUserId", "leadScore", "timezone", "preferredLanguage", "description")))
|
||||
.withSection(new QFieldSection("contactInformation", "Contact Information", new QIcon("contact_mail"), Tier.T2,
|
||||
List.of("secondaryEmail", "mobilePhone", "website", "linkedinUrl", "addressLine1", "addressLine2", "city", "state", "postalCode", "country",
|
||||
"doNotEmail", "doNotCall", "unsubscribeDate")))
|
||||
.withSection(new QFieldSection("attribution", new QIcon().withName("campaign"), Tier.T2,
|
||||
List.of("firstTouchUtmSource", "firstTouchUtmMedium", "firstTouchUtmCampaign", "firstTouchUtmContent", "firstTouchUtmTerm", "firstTouchLandingPage", "firstTouchReferrer", "firstTouchDate", "lastTouchUtmSource", "lastTouchUtmMedium", "lastTouchUtmCampaign", "lastTouchUtmContent", "lastTouchUtmTerm", "lastTouchLandingPage", "lastTouchReferrer", "lastTouchDate")).withLabel("Attribution"))
|
||||
.withSection(new QFieldSection("system", "System", new QIcon("settings"), Tier.T3,
|
||||
List.of("id", "lastActivityDate", "lastContactedDate", "createDate", "modifyDate")));
|
||||
"ownerUserId", "leadScore", "timezone", "preferredLanguage")))
|
||||
.withSection(new QFieldSection("online", new QIcon().withName("language"), Tier.T2,
|
||||
List.of("secondaryEmail", "mobilePhone", "website", "linkedinUrl")))
|
||||
.withSection(new QFieldSection("address", new QIcon().withName("location_on"), Tier.T2,
|
||||
List.of("addressLine1", "addressLine2", "city", "state", "postalCode", "country")))
|
||||
.withSection(new QFieldSection("communication", new QIcon().withName("settings"), Tier.T2,
|
||||
List.of("doNotEmail", "doNotCall", "unsubscribeDate")))
|
||||
.withSection(SectionFactory.defaultT2("description"))
|
||||
.withSection(SectionFactory.defaultT3("lastActivityDate", "lastContactedDate", "createDate", "modifyDate"));
|
||||
|
||||
///////////////////////////////////////////////////////////////
|
||||
// inject security fields and locks from CrmQBitConfig //
|
||||
///////////////////////////////////////////////////////////////
|
||||
CrmQBitConfig config = (CrmQBitConfig) QBitProductionContext.peekQBitConfig();
|
||||
if(config != null)
|
||||
{
|
||||
if(config.getSecurityFields() != null)
|
||||
{
|
||||
t1Section.setFieldNames(new MutableList<>(t1Section.getFieldNames()));
|
||||
for(QFieldMetaData fieldMetaData : config.getSecurityFields())
|
||||
{
|
||||
table.addField(fieldMetaData.clone());
|
||||
t1Section.getFieldNames().add(fieldMetaData.getName());
|
||||
}
|
||||
}
|
||||
|
||||
if(config.getRecordSecurityLocks() != null)
|
||||
{
|
||||
for(RecordSecurityLock lock : config.getRecordSecurityLocks())
|
||||
{
|
||||
table.withRecordSecurityLock(lock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (table);
|
||||
}
|
||||
@@ -127,67 +122,19 @@ public class Contact extends QRecordEntity
|
||||
|
||||
|
||||
|
||||
@QField(maxLength = 500, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String firstTouchUtmSource;
|
||||
|
||||
@QField(maxLength = 500, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String firstTouchUtmMedium;
|
||||
|
||||
@QField(maxLength = 500, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String firstTouchUtmCampaign;
|
||||
|
||||
@QField(maxLength = 500, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String firstTouchUtmContent;
|
||||
|
||||
@QField(maxLength = 500, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String firstTouchUtmTerm;
|
||||
|
||||
@QField(maxLength = 1000, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String firstTouchLandingPage;
|
||||
|
||||
@QField(maxLength = 1000, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String firstTouchReferrer;
|
||||
|
||||
@QField()
|
||||
private Instant firstTouchDate;
|
||||
|
||||
@QField(maxLength = 500, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String lastTouchUtmSource;
|
||||
|
||||
@QField(maxLength = 500, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String lastTouchUtmMedium;
|
||||
|
||||
@QField(maxLength = 500, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String lastTouchUtmCampaign;
|
||||
|
||||
@QField(maxLength = 500, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String lastTouchUtmContent;
|
||||
|
||||
@QField(maxLength = 500, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String lastTouchUtmTerm;
|
||||
|
||||
@QField(maxLength = 1000, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String lastTouchLandingPage;
|
||||
|
||||
@QField(maxLength = 1000, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String lastTouchReferrer;
|
||||
|
||||
@QField()
|
||||
private Instant lastTouchDate;
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Integer id;
|
||||
|
||||
@QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
@QField(isRequired = true, maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String firstName;
|
||||
|
||||
@QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
@QField(isRequired = true, maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String lastName;
|
||||
|
||||
@QField(maxLength = 320, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String email;
|
||||
|
||||
@QField(maxLength = 320, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String secondaryEmail;
|
||||
|
||||
@QField(maxLength = 50, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
@@ -211,7 +158,7 @@ public class Contact extends QRecordEntity
|
||||
@QField(possibleValueSourceName = LeadSource.TABLE_NAME)
|
||||
private Integer leadSourceId;
|
||||
|
||||
@QField()
|
||||
@QField(isRequired = true)
|
||||
private String ownerUserId;
|
||||
|
||||
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
@@ -1282,453 +1229,4 @@ public class Contact extends QRecordEntity
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for firstTouchUtmSource
|
||||
*******************************************************************************/
|
||||
public String getFirstTouchUtmSource()
|
||||
{
|
||||
return (this.firstTouchUtmSource);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for firstTouchUtmSource
|
||||
*******************************************************************************/
|
||||
public void setFirstTouchUtmSource(String firstTouchUtmSource)
|
||||
{
|
||||
this.firstTouchUtmSource = firstTouchUtmSource;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for firstTouchUtmSource
|
||||
*******************************************************************************/
|
||||
public Contact withFirstTouchUtmSource(String firstTouchUtmSource)
|
||||
{
|
||||
this.firstTouchUtmSource = firstTouchUtmSource;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for firstTouchUtmMedium
|
||||
*******************************************************************************/
|
||||
public String getFirstTouchUtmMedium()
|
||||
{
|
||||
return (this.firstTouchUtmMedium);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for firstTouchUtmMedium
|
||||
*******************************************************************************/
|
||||
public void setFirstTouchUtmMedium(String firstTouchUtmMedium)
|
||||
{
|
||||
this.firstTouchUtmMedium = firstTouchUtmMedium;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for firstTouchUtmMedium
|
||||
*******************************************************************************/
|
||||
public Contact withFirstTouchUtmMedium(String firstTouchUtmMedium)
|
||||
{
|
||||
this.firstTouchUtmMedium = firstTouchUtmMedium;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for firstTouchUtmCampaign
|
||||
*******************************************************************************/
|
||||
public String getFirstTouchUtmCampaign()
|
||||
{
|
||||
return (this.firstTouchUtmCampaign);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for firstTouchUtmCampaign
|
||||
*******************************************************************************/
|
||||
public void setFirstTouchUtmCampaign(String firstTouchUtmCampaign)
|
||||
{
|
||||
this.firstTouchUtmCampaign = firstTouchUtmCampaign;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for firstTouchUtmCampaign
|
||||
*******************************************************************************/
|
||||
public Contact withFirstTouchUtmCampaign(String firstTouchUtmCampaign)
|
||||
{
|
||||
this.firstTouchUtmCampaign = firstTouchUtmCampaign;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for firstTouchUtmContent
|
||||
*******************************************************************************/
|
||||
public String getFirstTouchUtmContent()
|
||||
{
|
||||
return (this.firstTouchUtmContent);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for firstTouchUtmContent
|
||||
*******************************************************************************/
|
||||
public void setFirstTouchUtmContent(String firstTouchUtmContent)
|
||||
{
|
||||
this.firstTouchUtmContent = firstTouchUtmContent;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for firstTouchUtmContent
|
||||
*******************************************************************************/
|
||||
public Contact withFirstTouchUtmContent(String firstTouchUtmContent)
|
||||
{
|
||||
this.firstTouchUtmContent = firstTouchUtmContent;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for firstTouchUtmTerm
|
||||
*******************************************************************************/
|
||||
public String getFirstTouchUtmTerm()
|
||||
{
|
||||
return (this.firstTouchUtmTerm);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for firstTouchUtmTerm
|
||||
*******************************************************************************/
|
||||
public void setFirstTouchUtmTerm(String firstTouchUtmTerm)
|
||||
{
|
||||
this.firstTouchUtmTerm = firstTouchUtmTerm;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for firstTouchUtmTerm
|
||||
*******************************************************************************/
|
||||
public Contact withFirstTouchUtmTerm(String firstTouchUtmTerm)
|
||||
{
|
||||
this.firstTouchUtmTerm = firstTouchUtmTerm;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for firstTouchLandingPage
|
||||
*******************************************************************************/
|
||||
public String getFirstTouchLandingPage()
|
||||
{
|
||||
return (this.firstTouchLandingPage);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for firstTouchLandingPage
|
||||
*******************************************************************************/
|
||||
public void setFirstTouchLandingPage(String firstTouchLandingPage)
|
||||
{
|
||||
this.firstTouchLandingPage = firstTouchLandingPage;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for firstTouchLandingPage
|
||||
*******************************************************************************/
|
||||
public Contact withFirstTouchLandingPage(String firstTouchLandingPage)
|
||||
{
|
||||
this.firstTouchLandingPage = firstTouchLandingPage;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for firstTouchReferrer
|
||||
*******************************************************************************/
|
||||
public String getFirstTouchReferrer()
|
||||
{
|
||||
return (this.firstTouchReferrer);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for firstTouchReferrer
|
||||
*******************************************************************************/
|
||||
public void setFirstTouchReferrer(String firstTouchReferrer)
|
||||
{
|
||||
this.firstTouchReferrer = firstTouchReferrer;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for firstTouchReferrer
|
||||
*******************************************************************************/
|
||||
public Contact withFirstTouchReferrer(String firstTouchReferrer)
|
||||
{
|
||||
this.firstTouchReferrer = firstTouchReferrer;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for firstTouchDate
|
||||
*******************************************************************************/
|
||||
public Instant getFirstTouchDate()
|
||||
{
|
||||
return (this.firstTouchDate);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for firstTouchDate
|
||||
*******************************************************************************/
|
||||
public void setFirstTouchDate(Instant firstTouchDate)
|
||||
{
|
||||
this.firstTouchDate = firstTouchDate;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for firstTouchDate
|
||||
*******************************************************************************/
|
||||
public Contact withFirstTouchDate(Instant firstTouchDate)
|
||||
{
|
||||
this.firstTouchDate = firstTouchDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for lastTouchUtmSource
|
||||
*******************************************************************************/
|
||||
public String getLastTouchUtmSource()
|
||||
{
|
||||
return (this.lastTouchUtmSource);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for lastTouchUtmSource
|
||||
*******************************************************************************/
|
||||
public void setLastTouchUtmSource(String lastTouchUtmSource)
|
||||
{
|
||||
this.lastTouchUtmSource = lastTouchUtmSource;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for lastTouchUtmSource
|
||||
*******************************************************************************/
|
||||
public Contact withLastTouchUtmSource(String lastTouchUtmSource)
|
||||
{
|
||||
this.lastTouchUtmSource = lastTouchUtmSource;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for lastTouchUtmMedium
|
||||
*******************************************************************************/
|
||||
public String getLastTouchUtmMedium()
|
||||
{
|
||||
return (this.lastTouchUtmMedium);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for lastTouchUtmMedium
|
||||
*******************************************************************************/
|
||||
public void setLastTouchUtmMedium(String lastTouchUtmMedium)
|
||||
{
|
||||
this.lastTouchUtmMedium = lastTouchUtmMedium;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for lastTouchUtmMedium
|
||||
*******************************************************************************/
|
||||
public Contact withLastTouchUtmMedium(String lastTouchUtmMedium)
|
||||
{
|
||||
this.lastTouchUtmMedium = lastTouchUtmMedium;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for lastTouchUtmCampaign
|
||||
*******************************************************************************/
|
||||
public String getLastTouchUtmCampaign()
|
||||
{
|
||||
return (this.lastTouchUtmCampaign);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for lastTouchUtmCampaign
|
||||
*******************************************************************************/
|
||||
public void setLastTouchUtmCampaign(String lastTouchUtmCampaign)
|
||||
{
|
||||
this.lastTouchUtmCampaign = lastTouchUtmCampaign;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for lastTouchUtmCampaign
|
||||
*******************************************************************************/
|
||||
public Contact withLastTouchUtmCampaign(String lastTouchUtmCampaign)
|
||||
{
|
||||
this.lastTouchUtmCampaign = lastTouchUtmCampaign;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for lastTouchUtmContent
|
||||
*******************************************************************************/
|
||||
public String getLastTouchUtmContent()
|
||||
{
|
||||
return (this.lastTouchUtmContent);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for lastTouchUtmContent
|
||||
*******************************************************************************/
|
||||
public void setLastTouchUtmContent(String lastTouchUtmContent)
|
||||
{
|
||||
this.lastTouchUtmContent = lastTouchUtmContent;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for lastTouchUtmContent
|
||||
*******************************************************************************/
|
||||
public Contact withLastTouchUtmContent(String lastTouchUtmContent)
|
||||
{
|
||||
this.lastTouchUtmContent = lastTouchUtmContent;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for lastTouchUtmTerm
|
||||
*******************************************************************************/
|
||||
public String getLastTouchUtmTerm()
|
||||
{
|
||||
return (this.lastTouchUtmTerm);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for lastTouchUtmTerm
|
||||
*******************************************************************************/
|
||||
public void setLastTouchUtmTerm(String lastTouchUtmTerm)
|
||||
{
|
||||
this.lastTouchUtmTerm = lastTouchUtmTerm;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for lastTouchUtmTerm
|
||||
*******************************************************************************/
|
||||
public Contact withLastTouchUtmTerm(String lastTouchUtmTerm)
|
||||
{
|
||||
this.lastTouchUtmTerm = lastTouchUtmTerm;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for lastTouchLandingPage
|
||||
*******************************************************************************/
|
||||
public String getLastTouchLandingPage()
|
||||
{
|
||||
return (this.lastTouchLandingPage);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for lastTouchLandingPage
|
||||
*******************************************************************************/
|
||||
public void setLastTouchLandingPage(String lastTouchLandingPage)
|
||||
{
|
||||
this.lastTouchLandingPage = lastTouchLandingPage;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for lastTouchLandingPage
|
||||
*******************************************************************************/
|
||||
public Contact withLastTouchLandingPage(String lastTouchLandingPage)
|
||||
{
|
||||
this.lastTouchLandingPage = lastTouchLandingPage;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for lastTouchReferrer
|
||||
*******************************************************************************/
|
||||
public String getLastTouchReferrer()
|
||||
{
|
||||
return (this.lastTouchReferrer);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for lastTouchReferrer
|
||||
*******************************************************************************/
|
||||
public void setLastTouchReferrer(String lastTouchReferrer)
|
||||
{
|
||||
this.lastTouchReferrer = lastTouchReferrer;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for lastTouchReferrer
|
||||
*******************************************************************************/
|
||||
public Contact withLastTouchReferrer(String lastTouchReferrer)
|
||||
{
|
||||
this.lastTouchReferrer = lastTouchReferrer;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for lastTouchDate
|
||||
*******************************************************************************/
|
||||
public Instant getLastTouchDate()
|
||||
{
|
||||
return (this.lastTouchDate);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for lastTouchDate
|
||||
*******************************************************************************/
|
||||
public void setLastTouchDate(Instant lastTouchDate)
|
||||
{
|
||||
this.lastTouchDate = lastTouchDate;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for lastTouchDate
|
||||
*******************************************************************************/
|
||||
public Contact withLastTouchDate(Instant lastTouchDate)
|
||||
{
|
||||
this.lastTouchDate = lastTouchDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for ContactTag table
|
||||
*******************************************************************************/
|
||||
package com.kingsrook.qbits.crm.core.model;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for ContactTag junction table -- many-to-many link between
|
||||
** contacts and tags.
|
||||
*******************************************************************************/
|
||||
@QMetaDataProducingEntity(
|
||||
produceTableMetaData = true,
|
||||
tableMetaDataCustomizer = ContactTag.TableMetaDataCustomizer.class
|
||||
)
|
||||
public class ContactTag extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "crmContactTag";
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static class TableMetaDataCustomizer implements MetaDataCustomizerInterface<QTableMetaData>
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public QTableMetaData customizeMetaData(QInstance qInstance, QTableMetaData table) throws QException
|
||||
{
|
||||
table
|
||||
.withIcon(new QIcon().withName("label"))
|
||||
.withRecordLabelFormat("Contact %s - Tag %s")
|
||||
.withRecordLabelFields("contactId", "tagId")
|
||||
.withUniqueKey(new UniqueKey("contactId", "tagId"))
|
||||
.withSection(SectionFactory.defaultT1("id", "contactId", "tagId"))
|
||||
.withSection(SectionFactory.defaultT3("createDate"));
|
||||
|
||||
return (table);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Integer id;
|
||||
|
||||
@QField(isRequired = true, possibleValueSourceName = Contact.TABLE_NAME)
|
||||
private Integer contactId;
|
||||
|
||||
@QField(isRequired = true, possibleValueSourceName = Tag.TABLE_NAME)
|
||||
private Integer tagId;
|
||||
|
||||
@QField(isEditable = false)
|
||||
private Instant createDate;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Default constructor
|
||||
*******************************************************************************/
|
||||
public ContactTag()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor that takes a QRecord
|
||||
*******************************************************************************/
|
||||
public ContactTag(QRecord record)
|
||||
{
|
||||
populateFromQRecord(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for id
|
||||
*******************************************************************************/
|
||||
public Integer getId()
|
||||
{
|
||||
return (this.id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for id
|
||||
*******************************************************************************/
|
||||
public void setId(Integer id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for id
|
||||
*******************************************************************************/
|
||||
public ContactTag withId(Integer id)
|
||||
{
|
||||
this.id = id;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for contactId
|
||||
*******************************************************************************/
|
||||
public Integer getContactId()
|
||||
{
|
||||
return (this.contactId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for contactId
|
||||
*******************************************************************************/
|
||||
public void setContactId(Integer contactId)
|
||||
{
|
||||
this.contactId = contactId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for contactId
|
||||
*******************************************************************************/
|
||||
public ContactTag withContactId(Integer contactId)
|
||||
{
|
||||
this.contactId = contactId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for tagId
|
||||
*******************************************************************************/
|
||||
public Integer getTagId()
|
||||
{
|
||||
return (this.tagId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for tagId
|
||||
*******************************************************************************/
|
||||
public void setTagId(Integer tagId)
|
||||
{
|
||||
this.tagId = tagId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for tagId
|
||||
*******************************************************************************/
|
||||
public ContactTag withTagId(Integer tagId)
|
||||
{
|
||||
this.tagId = tagId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for createDate
|
||||
*******************************************************************************/
|
||||
public Instant getCreateDate()
|
||||
{
|
||||
return (this.createDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for createDate
|
||||
*******************************************************************************/
|
||||
public void setCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for createDate
|
||||
*******************************************************************************/
|
||||
public ContactTag withCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for Currency table
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model;
|
||||
|
||||
|
||||
@@ -54,7 +32,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
)
|
||||
public class Currency extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "crm_currency";
|
||||
public static final String TABLE_NAME = "crmCurrency";
|
||||
|
||||
|
||||
|
||||
@@ -106,7 +84,7 @@ public class Currency extends QRecordEntity
|
||||
@QField(isRequired = true, defaultValue = "true")
|
||||
private Boolean isActive;
|
||||
|
||||
@QField(isEditable = false)
|
||||
@QField()
|
||||
private Instant rateLastUpdatedDate;
|
||||
|
||||
@QField(isEditable = false)
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for EmailList table
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model;
|
||||
|
||||
|
||||
@@ -35,9 +13,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildRecordListWidget;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildTable;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory;
|
||||
@@ -51,16 +26,11 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
@QMetaDataProducingEntity(
|
||||
producePossibleValueSource = true,
|
||||
produceTableMetaData = true,
|
||||
tableMetaDataCustomizer = EmailList.TableMetaDataCustomizer.class,
|
||||
childTables = {
|
||||
@ChildTable(childTableEntityClass = EmailListMember.class, joinFieldName = "emailListId",
|
||||
childJoin = @ChildJoin(enabled = true),
|
||||
childRecordListWidget = @ChildRecordListWidget(label = "Members", enabled = true, maxRows = 25))
|
||||
}
|
||||
tableMetaDataCustomizer = EmailList.TableMetaDataCustomizer.class
|
||||
)
|
||||
public class EmailList extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "crm_email_list";
|
||||
public static final String TABLE_NAME = "crmEmailList";
|
||||
|
||||
|
||||
|
||||
@@ -82,9 +52,8 @@ public class EmailList extends QRecordEntity
|
||||
.withRecordLabelFields("name")
|
||||
.withUniqueKey(new UniqueKey("name"))
|
||||
.withUniqueKey(new UniqueKey("slug"))
|
||||
.withUniqueKey(new UniqueKey("listKey"))
|
||||
.withSection(SectionFactory.defaultT1("id", "name", "listKey", "slug", "isActive", "isPublic", "sortOrder"))
|
||||
.withSection(SectionFactory.defaultT2("description", "memberCount", "requireDoubleOptIn", "consentText"))
|
||||
.withSection(SectionFactory.defaultT1("id", "name", "slug", "isActive", "isPublic", "sortOrder"))
|
||||
.withSection(SectionFactory.defaultT2("description", "memberCount"))
|
||||
.withSection(SectionFactory.defaultT3("createDate", "modifyDate"));
|
||||
|
||||
return (table);
|
||||
@@ -93,15 +62,6 @@ public class EmailList extends QRecordEntity
|
||||
|
||||
|
||||
|
||||
@QField(isRequired = true, maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String listKey;
|
||||
|
||||
@QField(isRequired = true, defaultValue = "true")
|
||||
private Boolean requireDoubleOptIn;
|
||||
|
||||
@QField()
|
||||
private String consentText;
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Integer id;
|
||||
|
||||
@@ -461,89 +421,4 @@ public class EmailList extends QRecordEntity
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for listKey
|
||||
*******************************************************************************/
|
||||
public String getListKey()
|
||||
{
|
||||
return (this.listKey);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for listKey
|
||||
*******************************************************************************/
|
||||
public void setListKey(String listKey)
|
||||
{
|
||||
this.listKey = listKey;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for listKey
|
||||
*******************************************************************************/
|
||||
public EmailList withListKey(String listKey)
|
||||
{
|
||||
this.listKey = listKey;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for requireDoubleOptIn
|
||||
*******************************************************************************/
|
||||
public Boolean getRequireDoubleOptIn()
|
||||
{
|
||||
return (this.requireDoubleOptIn);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for requireDoubleOptIn
|
||||
*******************************************************************************/
|
||||
public void setRequireDoubleOptIn(Boolean requireDoubleOptIn)
|
||||
{
|
||||
this.requireDoubleOptIn = requireDoubleOptIn;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for requireDoubleOptIn
|
||||
*******************************************************************************/
|
||||
public EmailList withRequireDoubleOptIn(Boolean requireDoubleOptIn)
|
||||
{
|
||||
this.requireDoubleOptIn = requireDoubleOptIn;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for consentText
|
||||
*******************************************************************************/
|
||||
public String getConsentText()
|
||||
{
|
||||
return (this.consentText);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for consentText
|
||||
*******************************************************************************/
|
||||
public void setConsentText(String consentText)
|
||||
{
|
||||
this.consentText = consentText;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for consentText
|
||||
*******************************************************************************/
|
||||
public EmailList withConsentText(String consentText)
|
||||
{
|
||||
this.consentText = consentText;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,39 +1,12 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for EmailListMember table
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmListMemberStatus;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.AiFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.DataClassification;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
@@ -57,7 +30,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
)
|
||||
public class EmailListMember extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "crm_email_list_member";
|
||||
public static final String TABLE_NAME = "crmEmailListMember";
|
||||
|
||||
|
||||
|
||||
@@ -79,41 +52,15 @@ public class EmailListMember extends QRecordEntity
|
||||
.withRecordLabelFields("emailListId", "email")
|
||||
.withUniqueKey(new UniqueKey("emailListId", "email"))
|
||||
.withSection(SectionFactory.defaultT1("id", "emailListId", "contactId", "email", "firstName", "status"))
|
||||
.withSection(SectionFactory.defaultT2("subscribedDate", "unsubscribedDate", "subscriptionSource", "formSubmissionId", "source", "consentText", "confirmationTokenHash", "confirmationSentDate", "confirmedDate"))
|
||||
.withSection(SectionFactory.defaultT2("subscribedDate", "unsubscribedDate", "subscriptionSource", "formSubmissionId"))
|
||||
.withSection(SectionFactory.defaultT3("createDate", "modifyDate"));
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// Only offer submissions linked to the selected contact. //
|
||||
////////////////////////////////////////////////////////////
|
||||
table.getField("formSubmissionId").withPossibleValueSourceFilter(new QQueryFilter(
|
||||
new QFilterCriteria("contactId", QCriteriaOperator.EQUALS, "${input.contactId}")));
|
||||
|
||||
table.getField("email").withSupplementalMetaData(new AiFieldMetaData().withClassification(DataClassification.PII));
|
||||
table.getField("firstName").withSupplementalMetaData(new AiFieldMetaData().withClassification(DataClassification.PII));
|
||||
table.getField("consentText").withSupplementalMetaData(new AiFieldMetaData().withClassification(DataClassification.PII));
|
||||
table.getField("confirmationTokenHash").withSupplementalMetaData(new AiFieldMetaData().withClassification(DataClassification.PII));
|
||||
|
||||
return (table);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@QField(maxLength = 64, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String confirmationTokenHash;
|
||||
|
||||
@QField()
|
||||
private Instant confirmationSentDate;
|
||||
|
||||
@QField()
|
||||
private Instant confirmedDate;
|
||||
|
||||
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String source;
|
||||
|
||||
@QField()
|
||||
private String consentText;
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Integer id;
|
||||
|
||||
@@ -123,7 +70,7 @@ public class EmailListMember extends QRecordEntity
|
||||
@QField(possibleValueSourceName = Contact.TABLE_NAME)
|
||||
private Integer contactId;
|
||||
|
||||
@QField(isRequired = true, maxLength = 320, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
@QField(isRequired = true, maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String email;
|
||||
|
||||
@QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
@@ -132,7 +79,7 @@ public class EmailListMember extends QRecordEntity
|
||||
@QField(isRequired = true, possibleValueSourceName = CrmListMemberStatus.NAME, defaultValue = "1")
|
||||
private Integer status;
|
||||
|
||||
@QField()
|
||||
@QField(isRequired = true)
|
||||
private Instant subscribedDate;
|
||||
|
||||
@QField()
|
||||
@@ -541,145 +488,4 @@ public class EmailListMember extends QRecordEntity
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for confirmationTokenHash
|
||||
*******************************************************************************/
|
||||
public String getConfirmationTokenHash()
|
||||
{
|
||||
return (this.confirmationTokenHash);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for confirmationTokenHash
|
||||
*******************************************************************************/
|
||||
public void setConfirmationTokenHash(String confirmationTokenHash)
|
||||
{
|
||||
this.confirmationTokenHash = confirmationTokenHash;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for confirmationTokenHash
|
||||
*******************************************************************************/
|
||||
public EmailListMember withConfirmationTokenHash(String confirmationTokenHash)
|
||||
{
|
||||
this.confirmationTokenHash = confirmationTokenHash;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for confirmationSentDate
|
||||
*******************************************************************************/
|
||||
public Instant getConfirmationSentDate()
|
||||
{
|
||||
return (this.confirmationSentDate);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for confirmationSentDate
|
||||
*******************************************************************************/
|
||||
public void setConfirmationSentDate(Instant confirmationSentDate)
|
||||
{
|
||||
this.confirmationSentDate = confirmationSentDate;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for confirmationSentDate
|
||||
*******************************************************************************/
|
||||
public EmailListMember withConfirmationSentDate(Instant confirmationSentDate)
|
||||
{
|
||||
this.confirmationSentDate = confirmationSentDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for confirmedDate
|
||||
*******************************************************************************/
|
||||
public Instant getConfirmedDate()
|
||||
{
|
||||
return (this.confirmedDate);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for confirmedDate
|
||||
*******************************************************************************/
|
||||
public void setConfirmedDate(Instant confirmedDate)
|
||||
{
|
||||
this.confirmedDate = confirmedDate;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for confirmedDate
|
||||
*******************************************************************************/
|
||||
public EmailListMember withConfirmedDate(Instant confirmedDate)
|
||||
{
|
||||
this.confirmedDate = confirmedDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for source
|
||||
*******************************************************************************/
|
||||
public String getSource()
|
||||
{
|
||||
return (this.source);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for source
|
||||
*******************************************************************************/
|
||||
public void setSource(String source)
|
||||
{
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for source
|
||||
*******************************************************************************/
|
||||
public EmailListMember withSource(String source)
|
||||
{
|
||||
this.source = source;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for consentText
|
||||
*******************************************************************************/
|
||||
public String getConsentText()
|
||||
{
|
||||
return (this.consentText);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for consentText
|
||||
*******************************************************************************/
|
||||
public void setConsentText(String consentText)
|
||||
{
|
||||
this.consentText = consentText;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for consentText
|
||||
*******************************************************************************/
|
||||
public EmailListMember withConsentText(String consentText)
|
||||
{
|
||||
this.consentText = consentText;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for FormSubmission table
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model;
|
||||
|
||||
|
||||
@@ -30,8 +8,6 @@ import java.time.Instant;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmFormConversionStatus;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmFormType;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.AiFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.DataClassification;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
@@ -49,7 +25,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for FormSubmission table -- raw website form submissions.
|
||||
** Captures web-specific metadata (UTM, referrer, hashed client identifiers) that does not belong on
|
||||
** Captures web-specific metadata (UTM, referrer, IP) that does not belong on
|
||||
** crm_contact. Submissions are converted to contacts via a process.
|
||||
*******************************************************************************/
|
||||
@QMetaDataProducingEntity(
|
||||
@@ -59,7 +35,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||
)
|
||||
public class FormSubmission extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "crm_form_submission";
|
||||
public static final String TABLE_NAME = "crmFormSubmission";
|
||||
|
||||
|
||||
|
||||
@@ -79,57 +55,29 @@ public class FormSubmission extends QRecordEntity
|
||||
.withIcon(new QIcon().withName("dynamic_form"))
|
||||
.withRecordLabelFormat("%s - %s %s")
|
||||
.withRecordLabelFields("formType", "firstName", "email")
|
||||
.withSection(SectionFactory.defaultT1("id", "formKey", "formType", "email", "firstName", "lastName"))
|
||||
.withSection(SectionFactory.defaultT2("company", "phone", "message", "marketingConsent", "consentText"))
|
||||
.withSection(SectionFactory.defaultT1("id", "formType", "email", "firstName", "lastName"))
|
||||
.withSection(SectionFactory.defaultT2("company", "phone", "message"))
|
||||
.withSection(new QFieldSection("utm", new QIcon().withName("campaign"), Tier.T2,
|
||||
List.of("sourceUrl", "landingPage", "utmSource", "utmMedium", "utmCampaign", "utmTerm", "utmContent", "referrer")))
|
||||
List.of("sourceUrl", "utmSource", "utmMedium", "utmCampaign", "utmTerm", "utmContent", "referrer")))
|
||||
.withSection(new QFieldSection("conversion", new QIcon().withName("swap_horiz"), Tier.T2,
|
||||
List.of("contactId", "conversionStatus", "convertedDate", "convertedByUserId")))
|
||||
.withSection(new QFieldSection("technical", new QIcon().withName("code"), Tier.T2,
|
||||
List.of("visitorKey", "clientHash", "metadata")))
|
||||
List.of("ipAddress", "userAgent", "metadata")))
|
||||
.withSection(SectionFactory.defaultT3("createDate", "modifyDate"));
|
||||
|
||||
table.getField("firstName").withSupplementalMetaData(new AiFieldMetaData().withClassification(DataClassification.PII));
|
||||
table.getField("lastName").withSupplementalMetaData(new AiFieldMetaData().withClassification(DataClassification.PII));
|
||||
table.getField("email").withSupplementalMetaData(new AiFieldMetaData().withClassification(DataClassification.PII));
|
||||
table.getField("phone").withSupplementalMetaData(new AiFieldMetaData().withClassification(DataClassification.PII));
|
||||
table.getField("company").withSupplementalMetaData(new AiFieldMetaData().withClassification(DataClassification.PII));
|
||||
table.getField("message").withSupplementalMetaData(new AiFieldMetaData().withClassification(DataClassification.PII));
|
||||
table.getField("consentText").withSupplementalMetaData(new AiFieldMetaData().withClassification(DataClassification.PII));
|
||||
table.getField("visitorKey").withSupplementalMetaData(new AiFieldMetaData().withClassification(DataClassification.PII));
|
||||
table.getField("clientHash").withSupplementalMetaData(new AiFieldMetaData().withClassification(DataClassification.PII));
|
||||
|
||||
return (table);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@QField(isRequired = true, maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String formKey;
|
||||
|
||||
@QField(defaultValue = "false")
|
||||
private Boolean marketingConsent;
|
||||
|
||||
@QField()
|
||||
private String consentText;
|
||||
|
||||
@QField(maxLength = 1000, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String landingPage;
|
||||
|
||||
@QField(maxLength = 64, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String visitorKey;
|
||||
|
||||
@QField(maxLength = 64, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String clientHash;
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Integer id;
|
||||
|
||||
@QField(isRequired = true, possibleValueSourceName = CrmFormType.NAME)
|
||||
private Integer formType;
|
||||
|
||||
@QField(isRequired = true, maxLength = 320, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
@QField(isRequired = true, maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String email;
|
||||
|
||||
@QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
@@ -150,19 +98,19 @@ public class FormSubmission extends QRecordEntity
|
||||
@QField(maxLength = 1000, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String sourceUrl;
|
||||
|
||||
@QField(maxLength = 500, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String utmSource;
|
||||
|
||||
@QField(maxLength = 500, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String utmMedium;
|
||||
|
||||
@QField(maxLength = 500, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String utmCampaign;
|
||||
|
||||
@QField(maxLength = 500, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String utmTerm;
|
||||
|
||||
@QField(maxLength = 500, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String utmContent;
|
||||
|
||||
@QField(maxLength = 1000, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
@@ -180,6 +128,12 @@ public class FormSubmission extends QRecordEntity
|
||||
@QField()
|
||||
private String convertedByUserId;
|
||||
|
||||
@QField(maxLength = 45, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String ipAddress;
|
||||
|
||||
@QField(maxLength = 500, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String userAgent;
|
||||
|
||||
@QField()
|
||||
private String metadata;
|
||||
|
||||
@@ -799,6 +753,68 @@ public class FormSubmission extends QRecordEntity
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for ipAddress
|
||||
*******************************************************************************/
|
||||
public String getIpAddress()
|
||||
{
|
||||
return (this.ipAddress);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for ipAddress
|
||||
*******************************************************************************/
|
||||
public void setIpAddress(String ipAddress)
|
||||
{
|
||||
this.ipAddress = ipAddress;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for ipAddress
|
||||
*******************************************************************************/
|
||||
public FormSubmission withIpAddress(String ipAddress)
|
||||
{
|
||||
this.ipAddress = ipAddress;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for userAgent
|
||||
*******************************************************************************/
|
||||
public String getUserAgent()
|
||||
{
|
||||
return (this.userAgent);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for userAgent
|
||||
*******************************************************************************/
|
||||
public void setUserAgent(String userAgent)
|
||||
{
|
||||
this.userAgent = userAgent;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for userAgent
|
||||
*******************************************************************************/
|
||||
public FormSubmission withUserAgent(String userAgent)
|
||||
{
|
||||
this.userAgent = userAgent;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for metadata
|
||||
*******************************************************************************/
|
||||
@@ -890,173 +906,4 @@ public class FormSubmission extends QRecordEntity
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for formKey
|
||||
*******************************************************************************/
|
||||
public String getFormKey()
|
||||
{
|
||||
return (this.formKey);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for formKey
|
||||
*******************************************************************************/
|
||||
public void setFormKey(String formKey)
|
||||
{
|
||||
this.formKey = formKey;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for formKey
|
||||
*******************************************************************************/
|
||||
public FormSubmission withFormKey(String formKey)
|
||||
{
|
||||
this.formKey = formKey;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for marketingConsent
|
||||
*******************************************************************************/
|
||||
public Boolean getMarketingConsent()
|
||||
{
|
||||
return (this.marketingConsent);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for marketingConsent
|
||||
*******************************************************************************/
|
||||
public void setMarketingConsent(Boolean marketingConsent)
|
||||
{
|
||||
this.marketingConsent = marketingConsent;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for marketingConsent
|
||||
*******************************************************************************/
|
||||
public FormSubmission withMarketingConsent(Boolean marketingConsent)
|
||||
{
|
||||
this.marketingConsent = marketingConsent;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for consentText
|
||||
*******************************************************************************/
|
||||
public String getConsentText()
|
||||
{
|
||||
return (this.consentText);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for consentText
|
||||
*******************************************************************************/
|
||||
public void setConsentText(String consentText)
|
||||
{
|
||||
this.consentText = consentText;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for consentText
|
||||
*******************************************************************************/
|
||||
public FormSubmission withConsentText(String consentText)
|
||||
{
|
||||
this.consentText = consentText;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for landingPage
|
||||
*******************************************************************************/
|
||||
public String getLandingPage()
|
||||
{
|
||||
return (this.landingPage);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for landingPage
|
||||
*******************************************************************************/
|
||||
public void setLandingPage(String landingPage)
|
||||
{
|
||||
this.landingPage = landingPage;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for landingPage
|
||||
*******************************************************************************/
|
||||
public FormSubmission withLandingPage(String landingPage)
|
||||
{
|
||||
this.landingPage = landingPage;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for visitorKey
|
||||
*******************************************************************************/
|
||||
public String getVisitorKey()
|
||||
{
|
||||
return (this.visitorKey);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for visitorKey
|
||||
*******************************************************************************/
|
||||
public void setVisitorKey(String visitorKey)
|
||||
{
|
||||
this.visitorKey = visitorKey;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for visitorKey
|
||||
*******************************************************************************/
|
||||
public FormSubmission withVisitorKey(String visitorKey)
|
||||
{
|
||||
this.visitorKey = visitorKey;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for clientHash
|
||||
*******************************************************************************/
|
||||
public String getClientHash()
|
||||
{
|
||||
return (this.clientHash);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for clientHash
|
||||
*******************************************************************************/
|
||||
public void setClientHash(String clientHash)
|
||||
{
|
||||
this.clientHash = clientHash;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for clientHash
|
||||
*******************************************************************************/
|
||||
public FormSubmission withClientHash(String clientHash)
|
||||
{
|
||||
this.clientHash = clientHash;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for Industry table
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model;
|
||||
|
||||
|
||||
@@ -52,7 +30,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
)
|
||||
public class Industry extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "crm_industry";
|
||||
public static final String TABLE_NAME = "crmIndustry";
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for LeadSource table
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model;
|
||||
|
||||
|
||||
@@ -52,7 +30,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
)
|
||||
public class LeadSource extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "crm_lead_source";
|
||||
public static final String TABLE_NAME = "crmLeadSource";
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for LifecycleStage table
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model;
|
||||
|
||||
|
||||
@@ -52,7 +30,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
)
|
||||
public class LifecycleStage extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "crm_lifecycle_stage";
|
||||
public static final String TABLE_NAME = "crmLifecycleStage";
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for Tag table
|
||||
*******************************************************************************/
|
||||
package com.kingsrook.qbits.crm.core.model;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for Tag table -- freeform tags that can be applied to
|
||||
** contacts, companies, and deals via junction tables.
|
||||
*******************************************************************************/
|
||||
@QMetaDataProducingEntity(
|
||||
producePossibleValueSource = true,
|
||||
produceTableMetaData = true,
|
||||
tableMetaDataCustomizer = Tag.TableMetaDataCustomizer.class
|
||||
)
|
||||
public class Tag extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "crmTag";
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static class TableMetaDataCustomizer implements MetaDataCustomizerInterface<QTableMetaData>
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public QTableMetaData customizeMetaData(QInstance qInstance, QTableMetaData table) throws QException
|
||||
{
|
||||
table
|
||||
.withIcon(new QIcon().withName("label"))
|
||||
.withRecordLabelFormat("%s")
|
||||
.withRecordLabelFields("name")
|
||||
.withUniqueKey(new UniqueKey("name"))
|
||||
.withSection(SectionFactory.defaultT1("id", "name", "color"))
|
||||
.withSection(SectionFactory.defaultT3("createDate", "modifyDate"));
|
||||
|
||||
return (table);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Integer id;
|
||||
|
||||
@QField(isRequired = true, maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String name;
|
||||
|
||||
@QField(maxLength = 7, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String color;
|
||||
|
||||
@QField(isEditable = false)
|
||||
private Instant createDate;
|
||||
|
||||
@QField(isEditable = false)
|
||||
private Instant modifyDate;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Default constructor
|
||||
*******************************************************************************/
|
||||
public Tag()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor that takes a QRecord
|
||||
*******************************************************************************/
|
||||
public Tag(QRecord record)
|
||||
{
|
||||
populateFromQRecord(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for id
|
||||
*******************************************************************************/
|
||||
public Integer getId()
|
||||
{
|
||||
return (this.id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for id
|
||||
*******************************************************************************/
|
||||
public void setId(Integer id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for id
|
||||
*******************************************************************************/
|
||||
public Tag withId(Integer id)
|
||||
{
|
||||
this.id = id;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for name
|
||||
*******************************************************************************/
|
||||
public String getName()
|
||||
{
|
||||
return (this.name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for name
|
||||
*******************************************************************************/
|
||||
public void setName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for name
|
||||
*******************************************************************************/
|
||||
public Tag withName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for color
|
||||
*******************************************************************************/
|
||||
public String getColor()
|
||||
{
|
||||
return (this.color);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for color
|
||||
*******************************************************************************/
|
||||
public void setColor(String color)
|
||||
{
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for color
|
||||
*******************************************************************************/
|
||||
public Tag withColor(String color)
|
||||
{
|
||||
this.color = color;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for createDate
|
||||
*******************************************************************************/
|
||||
public Instant getCreateDate()
|
||||
{
|
||||
return (this.createDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for createDate
|
||||
*******************************************************************************/
|
||||
public void setCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for createDate
|
||||
*******************************************************************************/
|
||||
public Tag withCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for modifyDate
|
||||
*******************************************************************************/
|
||||
public Instant getModifyDate()
|
||||
{
|
||||
return (this.modifyDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public void setModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public Tag withModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Possible value source enum for CRM activity priority levels.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model.enums;
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Possible value source enum for CRM assignment strategies.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model.enums;
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Possible value source enum for CRM assignable entity types.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model.enums;
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/*******************************************************************************
|
||||
** Possible value source enum for CRM audit log actions.
|
||||
*******************************************************************************/
|
||||
package com.kingsrook.qbits.crm.core.model.enums;
|
||||
|
||||
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingPossibleValueEnum;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Enum of CRM audit log actions tracked for entity changes.
|
||||
*******************************************************************************/
|
||||
@QMetaDataProducingPossibleValueEnum()
|
||||
public enum CrmAuditAction implements PossibleValueEnum<Integer>
|
||||
{
|
||||
CREATED(1, "Created"),
|
||||
UPDATED(2, "Updated"),
|
||||
DELETED(3, "Deleted"),
|
||||
STAGE_CHANGED(4, "Stage Changed"),
|
||||
OWNER_CHANGED(5, "Owner Changed"),
|
||||
SCORE_CHANGED(6, "Score Changed"),
|
||||
MERGED(7, "Merged"),
|
||||
CONSENT_CHANGED(8, "Consent Changed"),
|
||||
TAG_CHANGED(9, "Tag Changed"),
|
||||
ENROLLED(10, "Enrolled"),
|
||||
UNENROLLED(11, "Unenrolled"),
|
||||
IMPORTED(12, "Imported"),
|
||||
ANONYMIZED(13, "Anonymized"),
|
||||
FORM_CONVERTED(14, "Form Converted");
|
||||
|
||||
private final Integer id;
|
||||
private final String label;
|
||||
|
||||
public static final String NAME = "CrmAuditAction";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
*******************************************************************************/
|
||||
CrmAuditAction(Integer id, String label)
|
||||
{
|
||||
this.id = id;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Get instance by id
|
||||
*******************************************************************************/
|
||||
public static CrmAuditAction getById(Integer id)
|
||||
{
|
||||
if(id == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
for(CrmAuditAction value : CrmAuditAction.values())
|
||||
{
|
||||
if(Objects.equals(value.id, id))
|
||||
{
|
||||
return (value);
|
||||
}
|
||||
}
|
||||
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for id
|
||||
*******************************************************************************/
|
||||
public Integer getId()
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for label
|
||||
*******************************************************************************/
|
||||
public String getLabel()
|
||||
{
|
||||
return label;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Integer getPossibleValueId()
|
||||
{
|
||||
return (getId());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getPossibleValueLabel()
|
||||
{
|
||||
return (getLabel());
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Possible value source enum for CRM email bounce types.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model.enums;
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Possible value source enum for CRM company types.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model.enums;
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Possible value source enum for CRM consent statuses.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model.enums;
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Possible value source enum for CRM consent types.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model.enums;
|
||||
|
||||
|
||||
@@ -40,8 +18,7 @@ public enum CrmConsentType implements PossibleValueEnum<Integer>
|
||||
EMAIL(1, "Email"),
|
||||
CALL(2, "Call"),
|
||||
SMS(3, "SMS"),
|
||||
DATA_PROCESSING(4, "Data Processing"),
|
||||
REPLY_TO_INQUIRY(5, "Reply to inquiry");
|
||||
DATA_PROCESSING(4, "Data Processing");
|
||||
|
||||
private final Integer id;
|
||||
private final String label;
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Possible value source enum for CRM deal priority levels.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model.enums;
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Possible value source enum for CRM direction (inbound/outbound).
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model.enums;
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Possible value source enum for CRM email statuses.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model.enums;
|
||||
|
||||
|
||||
@@ -40,13 +18,7 @@ public enum CrmEmailStatus implements PossibleValueEnum<Integer>
|
||||
DRAFT(1, "Draft"),
|
||||
SENT(2, "Sent"),
|
||||
DELIVERED(3, "Delivered"),
|
||||
FAILED(4, "Failed"),
|
||||
QUEUED(5, "Queued"),
|
||||
BOUNCED(6, "Bounced"),
|
||||
COMPLAINED(7, "Complained"),
|
||||
OPENED(8, "Opened"),
|
||||
CLICKED(9, "Clicked"),
|
||||
SUPPRESSED(10, "Suppressed");
|
||||
FAILED(4, "Failed");
|
||||
|
||||
private final Integer id;
|
||||
private final String label;
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Possible value source enum for CRM sequence enrollment statuses.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model.enums;
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Possible value source enum for CRM entity types.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model.enums;
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Possible value source enum for CRM form conversion statuses.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model.enums;
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Possible value source enum for CRM form types.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model.enums;
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Possible value source enum for CRM goal periods.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model.enums;
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Possible value source enum for CRM legal basis types (GDPR).
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model.enums;
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Possible value source enum for CRM list member statuses.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model.enums;
|
||||
|
||||
|
||||
@@ -40,8 +18,7 @@ public enum CrmListMemberStatus implements PossibleValueEnum<Integer>
|
||||
SUBSCRIBED(1, "Subscribed"),
|
||||
UNSUBSCRIBED(2, "Unsubscribed"),
|
||||
BOUNCED(3, "Bounced"),
|
||||
CLEANED(4, "Cleaned"),
|
||||
PENDING_CONFIRMATION(5, "Pending confirmation");
|
||||
CLEANED(4, "Cleaned");
|
||||
|
||||
private final Integer id;
|
||||
private final String label;
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Possible value source enum for CRM activity participant roles.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model.enums;
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Possible value source enum for CRM scorable entity types.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model.enums;
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Possible value source enum for CRM scoring rule operators.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model.enums;
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Possible value source enum for CRM sequence step types.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model.enums;
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Possible value source enum for CRM sync statuses.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model.enums;
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Possible value source enum for CRM deal win/loss types.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.model.enums;
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
/*******************************************************************************
|
||||
** Bulk action process to add a tag to selected contact records. Inserts a
|
||||
** junction row in crmContactTag if one does not already exist.
|
||||
*******************************************************************************/
|
||||
package com.kingsrook.qbits.crm.core.processes;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.crm.core.model.Contact;
|
||||
import com.kingsrook.qbits.crm.core.model.ContactTag;
|
||||
import com.kingsrook.qbits.crm.core.model.Tag;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** MetaDataProducer + BackendStep for bulk-adding a tag to contacts.
|
||||
** For each selected record, inserts a crmContactTag junction row if one
|
||||
** does not already exist for that contactId + tagId combination.
|
||||
*******************************************************************************/
|
||||
public class BulkAddTagProcess implements BackendStep, MetaDataProducerInterface<QProcessMetaData>
|
||||
{
|
||||
public static final String NAME = "bulkAddTag";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Produce the process metadata.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QProcessMetaData produce(QInstance qInstance) throws QException
|
||||
{
|
||||
return new QProcessMetaData()
|
||||
.withName(NAME)
|
||||
.withLabel("Bulk Add Tag")
|
||||
.withTableName(Contact.TABLE_NAME)
|
||||
.withIcon(new QIcon().withName("label"))
|
||||
.withStepList(List.of(
|
||||
new QBackendStepMetaData()
|
||||
.withName("execute")
|
||||
.withCode(new QCodeReference(BulkAddTagProcess.class))
|
||||
.withInputData(new QFunctionInputMetaData().withFieldList(List.of(
|
||||
new QFieldMetaData("tagId", QFieldType.INTEGER).withIsRequired(true).withPossibleValueSourceName(Tag.TABLE_NAME),
|
||||
new QFieldMetaData("recordIds", QFieldType.STRING).withIsRequired(true).withLabel("Record IDs (comma-separated)")
|
||||
)))
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Execute: for each record id, insert a junction row if not exists.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void run(RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
Integer tagId = input.getValueInteger("tagId");
|
||||
String recordIds = input.getValueString("recordIds");
|
||||
|
||||
Integer addedCount = 0;
|
||||
Integer skippedCount = 0;
|
||||
|
||||
for(String idStr : recordIds.split(","))
|
||||
{
|
||||
Integer contactId = Integer.parseInt(idStr.trim());
|
||||
|
||||
/////////////////////////////////////////////////////////////
|
||||
// check if the junction row already exists //
|
||||
/////////////////////////////////////////////////////////////
|
||||
QueryOutput existingTags = new QueryAction().execute(
|
||||
new QueryInput(ContactTag.TABLE_NAME)
|
||||
.withFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("contactId", QCriteriaOperator.EQUALS, contactId))
|
||||
.withCriteria(new QFilterCriteria("tagId", QCriteriaOperator.EQUALS, tagId))));
|
||||
|
||||
if(existingTags.getRecords().isEmpty())
|
||||
{
|
||||
new InsertAction().execute(
|
||||
new InsertInput(ContactTag.TABLE_NAME).withRecordEntity(
|
||||
new ContactTag()
|
||||
.withContactId(contactId)
|
||||
.withTagId(tagId)));
|
||||
addedCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
skippedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
output.addValue("addedCount", addedCount);
|
||||
output.addValue("skippedCount", skippedCount);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*******************************************************************************
|
||||
** Bulk action process to assign a new owner to selected records. Works on
|
||||
** any table that has an ownerUserId field.
|
||||
*******************************************************************************/
|
||||
package com.kingsrook.qbits.crm.core.processes;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.crm.core.model.Contact;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.Status;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** MetaDataProducer for the BulkAssignOwner table-level process. Uses the
|
||||
** StreamedETL pattern with a transform step that sets ownerUserId on each
|
||||
** record to the newOwnerUserId input value.
|
||||
*******************************************************************************/
|
||||
public class BulkAssignOwnerProcess implements MetaDataProducerInterface<QProcessMetaData>
|
||||
{
|
||||
public static final String NAME = "bulkAssignOwner";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Produce the process metadata.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QProcessMetaData produce(QInstance qInstance) throws QException
|
||||
{
|
||||
QProcessMetaData process = StreamedETLWithFrontendProcess.processMetaDataBuilder()
|
||||
.withName(NAME)
|
||||
.withLabel("Bulk Assign Owner")
|
||||
.withTableName(Contact.TABLE_NAME)
|
||||
.withIcon(new QIcon().withName("person_pin"))
|
||||
.withTransformStepClass(BulkAssignOwnerTransformStep.class)
|
||||
.withPreviewMessage("Records to be assigned a new owner")
|
||||
.withReviewStepRecordFields(List.of(
|
||||
new QFieldMetaData("id", QFieldType.INTEGER),
|
||||
new QFieldMetaData("ownerUserId", QFieldType.STRING).withLabel("Current Owner")
|
||||
))
|
||||
.getProcessMetaData();
|
||||
|
||||
return (process);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Transform step that sets ownerUserId on each record.
|
||||
*******************************************************************************/
|
||||
public static class BulkAssignOwnerTransformStep extends AbstractTransformStep
|
||||
{
|
||||
private ProcessSummaryLine okLine = new ProcessSummaryLine(Status.OK)
|
||||
.withSingularFutureMessage("will be assigned a new owner")
|
||||
.withPluralFutureMessage("will be assigned a new owner")
|
||||
.withSingularPastMessage("was assigned a new owner")
|
||||
.withPluralPastMessage("were assigned a new owner");
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Set ownerUserId on each record from the newOwnerUserId input.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void runOnePage(RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
String newOwnerUserId = input.getValueString("newOwnerUserId");
|
||||
|
||||
for(QRecord record : input.getRecords())
|
||||
{
|
||||
record.setValue("ownerUserId", newOwnerUserId);
|
||||
output.addRecord(record);
|
||||
okLine.incrementCountAndAddPrimaryKey(record.getValueInteger("id"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Provide process summary.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public ArrayList<ProcessSummaryLineInterface> getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen)
|
||||
{
|
||||
ArrayList<ProcessSummaryLineInterface> rs = new ArrayList<>();
|
||||
okLine.addSelfToListIfAnyCount(rs);
|
||||
return (rs);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*******************************************************************************
|
||||
** Bulk action process to remove a tag from selected contact records. Deletes
|
||||
** the junction row in crmContactTag.
|
||||
*******************************************************************************/
|
||||
package com.kingsrook.qbits.crm.core.processes;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.crm.core.model.Contact;
|
||||
import com.kingsrook.qbits.crm.core.model.ContactTag;
|
||||
import com.kingsrook.qbits.crm.core.model.Tag;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** MetaDataProducer + BackendStep for bulk-removing a tag from contacts.
|
||||
** For each selected record, deletes the crmContactTag junction row if it
|
||||
** exists for that contactId + tagId combination.
|
||||
*******************************************************************************/
|
||||
public class BulkRemoveTagProcess implements BackendStep, MetaDataProducerInterface<QProcessMetaData>
|
||||
{
|
||||
public static final String NAME = "bulkRemoveTag";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Produce the process metadata.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QProcessMetaData produce(QInstance qInstance) throws QException
|
||||
{
|
||||
return new QProcessMetaData()
|
||||
.withName(NAME)
|
||||
.withLabel("Bulk Remove Tag")
|
||||
.withTableName(Contact.TABLE_NAME)
|
||||
.withIcon(new QIcon().withName("label_off"))
|
||||
.withStepList(List.of(
|
||||
new QBackendStepMetaData()
|
||||
.withName("execute")
|
||||
.withCode(new QCodeReference(BulkRemoveTagProcess.class))
|
||||
.withInputData(new QFunctionInputMetaData().withFieldList(List.of(
|
||||
new QFieldMetaData("tagId", QFieldType.INTEGER).withIsRequired(true).withPossibleValueSourceName(Tag.TABLE_NAME),
|
||||
new QFieldMetaData("recordIds", QFieldType.STRING).withIsRequired(true).withLabel("Record IDs (comma-separated)")
|
||||
)))
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Execute: for each record id, delete the junction row if it exists.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void run(RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
Integer tagId = input.getValueInteger("tagId");
|
||||
String recordIds = input.getValueString("recordIds");
|
||||
|
||||
Integer removedCount = 0;
|
||||
Integer skippedCount = 0;
|
||||
|
||||
for(String idStr : recordIds.split(","))
|
||||
{
|
||||
Integer contactId = Integer.parseInt(idStr.trim());
|
||||
|
||||
/////////////////////////////////////////////////////////////
|
||||
// find the junction row //
|
||||
/////////////////////////////////////////////////////////////
|
||||
QueryOutput existingTags = new QueryAction().execute(
|
||||
new QueryInput(ContactTag.TABLE_NAME)
|
||||
.withFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("contactId", QCriteriaOperator.EQUALS, contactId))
|
||||
.withCriteria(new QFilterCriteria("tagId", QCriteriaOperator.EQUALS, tagId))));
|
||||
|
||||
if(!existingTags.getRecords().isEmpty())
|
||||
{
|
||||
QRecord tagRecord = existingTags.getRecords().get(0);
|
||||
new DeleteAction().execute(
|
||||
new DeleteInput(ContactTag.TABLE_NAME)
|
||||
.withPrimaryKeys(List.of(tagRecord.getValueInteger("id"))));
|
||||
removedCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
skippedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
output.addValue("removedCount", removedCount);
|
||||
output.addValue("skippedCount", skippedCount);
|
||||
}
|
||||
|
||||
}
|
||||
+36
-25
@@ -1,34 +1,13 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Process to convert a form submission into a CRM contact. Handles duplicate
|
||||
** detection by email, creates the contact if needed, updates the form
|
||||
** submission record, and logs a Note activity on the contact.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.processes;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.crm.activities.model.Activity;
|
||||
import com.kingsrook.qbits.crm.activities.model.ActivityType;
|
||||
import com.kingsrook.qbits.crm.core.model.Contact;
|
||||
@@ -42,6 +21,7 @@ import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
@@ -53,17 +33,48 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Backend operation used by the streamed CRM actions for converting a form submission into a
|
||||
** MetaDataProducer + BackendStep for converting a form submission into a
|
||||
** contact record. Deduplicates on email, updates the form with conversion
|
||||
** data, and creates a Note activity on the contact.
|
||||
*******************************************************************************/
|
||||
public class ConvertFormToContactProcess implements BackendStep
|
||||
public class ConvertFormToContactProcess implements BackendStep, MetaDataProducerInterface<QProcessMetaData>
|
||||
{
|
||||
public static final String NAME = "crm_convert_form_to_contact";
|
||||
public static final String NAME = "convertFormToContact";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Produce the process metadata.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QProcessMetaData produce(QInstance qInstance) throws QException
|
||||
{
|
||||
return new QProcessMetaData()
|
||||
.withName(NAME)
|
||||
.withLabel("Convert Form to Contact")
|
||||
.withTableName(FormSubmission.TABLE_NAME)
|
||||
.withIcon(new QIcon().withName("swap_horiz"))
|
||||
.withStepList(List.of(
|
||||
new QBackendStepMetaData()
|
||||
.withName("execute")
|
||||
.withCode(new QCodeReference(ConvertFormToContactProcess.class))
|
||||
.withInputData(new QFunctionInputMetaData().withFieldList(List.of(
|
||||
new QFieldMetaData("formSubmissionId", QFieldType.INTEGER).withIsRequired(true)
|
||||
)))
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,29 +1,7 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Process to detect duplicate contacts (by email) or companies (by domain).
|
||||
** Returns groups where count > 1.
|
||||
*******************************************************************************/
|
||||
|
||||
package com.kingsrook.qbits.crm.core.processes;
|
||||
|
||||
|
||||
@@ -41,15 +19,46 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutp
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Backend operation used by the streamed CRM actions for detecting duplicate contacts or companies.
|
||||
** MetaDataProducer + BackendStep for detecting duplicate contacts or companies.
|
||||
*******************************************************************************/
|
||||
public class DetectDuplicatesProcess implements BackendStep
|
||||
public class DetectDuplicatesProcess implements BackendStep, MetaDataProducerInterface<QProcessMetaData>
|
||||
{
|
||||
public static final String NAME = "crm_detect_duplicates";
|
||||
public static final String NAME = "detectDuplicates";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Produce the process metadata.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QProcessMetaData produce(QInstance qInstance) throws QException
|
||||
{
|
||||
return new QProcessMetaData()
|
||||
.withName(NAME)
|
||||
.withLabel("Detect Duplicates")
|
||||
.withIcon(new QIcon().withName("find_replace"))
|
||||
.withStepList(List.of(
|
||||
new QBackendStepMetaData()
|
||||
.withName("execute")
|
||||
.withCode(new QCodeReference(DetectDuplicatesProcess.class))
|
||||
.withInputData(new QFunctionInputMetaData().withFieldList(List.of(
|
||||
new QFieldMetaData("entityType", QFieldType.STRING).withIsRequired(true)
|
||||
)))
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Loaded 100 of 304 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user